rename test directory to avoid name clash with Makefile target

Tue, 07 Feb 2023 21:55:37 +0100

author
Mike Becker <universe@uap-core.de>
date
Tue, 07 Feb 2023 21:55:37 +0100
changeset 653
e081643aae2a
parent 652
bf817b825ed2
child 654
c9d008861178

rename test directory to avoid name clash with Makefile target

CMakeLists.txt file | annotate | diff | comparison | revisions
test/.clang-tidy file | annotate | diff | comparison | revisions
test/CMakeLists.txt file | annotate | diff | comparison | revisions
test/selftest.cpp file | annotate | diff | comparison | revisions
test/test_allocator.cpp file | annotate | diff | comparison | revisions
test/test_basic_mempool.cpp file | annotate | diff | comparison | revisions
test/test_buffer.cpp file | annotate | diff | comparison | revisions
test/test_compare.cpp file | annotate | diff | comparison | revisions
test/test_hash_key.cpp file | annotate | diff | comparison | revisions
test/test_list.cpp file | annotate | diff | comparison | revisions
test/test_map.cpp file | annotate | diff | comparison | revisions
test/test_printf.cpp file | annotate | diff | comparison | revisions
test/test_string.cpp file | annotate | diff | comparison | revisions
test/test_tree.cpp file | annotate | diff | comparison | revisions
test/test_utils.cpp file | annotate | diff | comparison | revisions
test/util_allocator.cpp file | annotate | diff | comparison | revisions
test/util_allocator.h file | annotate | diff | comparison | revisions
tests/.clang-tidy file | annotate | diff | comparison | revisions
tests/CMakeLists.txt file | annotate | diff | comparison | revisions
tests/selftest.cpp file | annotate | diff | comparison | revisions
tests/test_allocator.cpp file | annotate | diff | comparison | revisions
tests/test_basic_mempool.cpp file | annotate | diff | comparison | revisions
tests/test_buffer.cpp file | annotate | diff | comparison | revisions
tests/test_compare.cpp file | annotate | diff | comparison | revisions
tests/test_hash_key.cpp file | annotate | diff | comparison | revisions
tests/test_list.cpp file | annotate | diff | comparison | revisions
tests/test_map.cpp file | annotate | diff | comparison | revisions
tests/test_printf.cpp file | annotate | diff | comparison | revisions
tests/test_string.cpp file | annotate | diff | comparison | revisions
tests/test_tree.cpp file | annotate | diff | comparison | revisions
tests/test_utils.cpp file | annotate | diff | comparison | revisions
tests/util_allocator.cpp file | annotate | diff | comparison | revisions
tests/util_allocator.h file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Tue Feb 07 21:53:06 2023 +0100
+++ b/CMakeLists.txt	Tue Feb 07 21:55:37 2023 +0100
@@ -15,7 +15,7 @@
 
 # Tests
 enable_testing()
-add_subdirectory(test)
+add_subdirectory(tests)
 
 # Web Documentation
 add_subdirectory(docs/src)
--- a/test/.clang-tidy	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-# Disable static initialization warning for test code
-Checks: '-cert-err58-cpp'
--- a/test/CMakeLists.txt	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-# Load Google Test Framework
-set(CMAKE_CXX_STANDARD 17)
-
-include(FetchContent)
-FetchContent_Declare(
-        googletest
-        GIT_REPOSITORY https://github.com/google/googletest.git
-        GIT_TAG e2239ee6043f73722e7aa812a459f54a28552929 # release 1.11.0
-)
-# For Windows: Prevent overriding the parent project's compiler/linker settings
-set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
-FetchContent_MakeAvailable(googletest)
-include(GoogleTest)
-message(STATUS "Google Test made available")
-
-add_executable(ucxtest
-        test_utils.cpp
-        test_allocator.cpp
-        test_compare.cpp
-        test_string.cpp
-        test_buffer.cpp
-        test_list.cpp
-        test_tree.cpp
-        test_hash_key.cpp
-        test_map.cpp
-        test_basic_mempool.cpp
-        test_printf.cpp
-        selftest.cpp
-        util_allocator.cpp
-        )
-target_link_libraries(ucxtest PRIVATE ucx_static gtest_main)
-gtest_discover_tests(ucxtest)
--- a/test/selftest.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include <gtest/gtest.h>
-#include <cx/common.h>
-
-TEST(SelfTest, BasicAssertion) {
-    EXPECT_EQ(7 * 6, 42);
-}
-
-TEST(SelfTest, UcxVersion) {
-    EXPECT_GE(UCX_VERSION_MAJOR, 3);
-    EXPECT_GE(UCX_VERSION, 3 << 16);
-}
--- a/test/test_allocator.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/allocator.h"
-#include <gtest/gtest.h>
-
-TEST(Allocator, DefaultAllocator) {
-    cx_allocator_class *clazz = cxDefaultAllocator->cl;
-    ASSERT_NE(clazz, nullptr);
-}
-
-TEST(Allocator, DefaultMalloc) {
-    void *test = cxMalloc(cxDefaultAllocator, 16);
-    ASSERT_NE(test, nullptr);
-    free(test);
-}
-
-TEST(Allocator, DefaultRealloc) {
-    void *test = calloc(8, 1);
-    memcpy(test, "Test", 5);
-    test = cxRealloc(cxDefaultAllocator, test, 16);
-    ASSERT_NE(test, nullptr);
-    EXPECT_STREQ(reinterpret_cast<char *>(test), "Test");
-    free(test);
-}
-
-TEST(Allocator, Reallocate) {
-    void *test = calloc(8, 1);
-    memcpy(test, "Test", 5);
-    int ret = cxReallocate(cxDefaultAllocator, &test, 16);
-    EXPECT_EQ(ret, 0);
-    ASSERT_NE(test, nullptr);
-    EXPECT_STREQ(reinterpret_cast<char *>(test), "Test");
-    free(test);
-}
-
-TEST(Allocator, DefaultCalloc) {
-    char *test = reinterpret_cast<char *>(cxCalloc(cxDefaultAllocator, 8, 2));
-    ASSERT_NE(test, nullptr);
-    for (int i = 0; i < 16; i++) ASSERT_EQ(test[i], 0);
-    free(test);
-}
-
-TEST(Allocator, DefaultFree) {
-    void *test = malloc(16);
-    EXPECT_NO_FATAL_FAILURE(
-            cxFree(cxDefaultAllocator, test);
-    );
-}
-
-TEST(Allocator, FailingReallocate) {
-    // Mock an allocator that always returns nullptr on realloc
-    cx_allocator_class mock_cl;
-    mock_cl.realloc = [](
-            [[maybe_unused]]void *p,
-            [[maybe_unused]]void *d,
-            [[maybe_unused]]size_t n
-    ) -> void * { return nullptr; };
-    cx_allocator_s mock{&mock_cl, nullptr};
-
-    void *test = calloc(8, 1);
-    memcpy(test, "Test", 5);
-    void *original = test;
-    int ret = cxReallocate(&mock, &test, 16);
-    // non-zero return code because of the failure
-    EXPECT_NE(ret, 0);
-    // the test pointer was not changed and still points to the same memory
-    EXPECT_EQ(test, original);
-    EXPECT_STREQ(reinterpret_cast<char *>(test), "Test");
-    free(test);
-}
--- a/test/test_basic_mempool.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/basic_mempool.h"
-#include "util_allocator.h"
-#include <gtest/gtest.h>
-
-class CxBasicMempool : public ::testing::Test {
-protected:
-    CxMempool *pool = nullptr;
-
-    void TearDown() override {
-        if (pool != nullptr) {
-            cxMempoolDestroy(pool);
-        }
-    }
-};
-
-TEST_F(CxBasicMempool, Create) {
-    pool = cxBasicMempoolCreate(16);
-    ASSERT_NE(pool->allocator, nullptr);
-    ASSERT_NE(pool->cl, nullptr);
-    EXPECT_NE(pool->cl->destroy, nullptr);
-    ASSERT_NE(pool->allocator->cl, nullptr);
-    EXPECT_EQ(pool->allocator->data, pool);
-    EXPECT_NE(pool->allocator->cl->malloc, nullptr);
-    EXPECT_NE(pool->allocator->cl->calloc, nullptr);
-    EXPECT_NE(pool->allocator->cl->realloc, nullptr);
-    EXPECT_NE(pool->allocator->cl->free, nullptr);
-
-    auto basic_pool = reinterpret_cast<cx_basic_mempool_s *>(pool);
-    EXPECT_EQ(basic_pool->size, 16);
-    EXPECT_EQ(basic_pool->ndata, 0);
-    EXPECT_NE(basic_pool->data, nullptr);
-}
-
-TEST_F(CxBasicMempool, malloc) {
-    pool = cxBasicMempoolCreate(4);
-    auto basic_pool = reinterpret_cast<cx_basic_mempool_s *>(pool);
-    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
-    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
-    EXPECT_EQ(basic_pool->ndata, 2);
-    EXPECT_EQ(basic_pool->size, 4);
-    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
-    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
-    EXPECT_EQ(basic_pool->ndata, 4);
-    EXPECT_EQ(basic_pool->size, 4);
-    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
-    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
-    EXPECT_EQ(basic_pool->ndata, 6);
-    EXPECT_GE(basic_pool->size, 6);
-}
-
-TEST_F(CxBasicMempool, calloc) {
-    pool = cxBasicMempoolCreate(4);
-
-    auto test = (int *) cxCalloc(pool->allocator, 2, sizeof(int));
-    ASSERT_NE(test, nullptr);
-    EXPECT_EQ(test[0], 0);
-    EXPECT_EQ(test[1], 0);
-}
-
-static unsigned test_destructor_called = 0;
-
-static void test_destructor([[maybe_unused]] void *mem) {
-    test_destructor_called++;
-}
-
-TEST_F(CxBasicMempool, destructor) {
-    pool = cxBasicMempoolCreate(4);
-    auto data = cxMalloc(pool->allocator, sizeof(int));
-    *((int *) data) = 13;
-    cxMempoolSetDestructor(pool, data, test_destructor);
-    EXPECT_EQ(*((int *) data), 13);
-    test_destructor_called = 0;
-    cxFree(pool->allocator, data);
-    EXPECT_EQ(test_destructor_called, 1);
-    data = cxMalloc(pool->allocator, sizeof(int));
-    cxMempoolSetDestructor(pool, data, test_destructor);
-    cxMempoolDestroy(pool);
-    pool = nullptr;
-    EXPECT_EQ(test_destructor_called, 2);
-}
-
-TEST_F(CxBasicMempool, realloc) {
-    pool = cxBasicMempoolCreate(4);
-    auto data = cxMalloc(pool->allocator, sizeof(int));
-    *((int *) data) = 13;
-    cxMempoolSetDestructor(pool, data, test_destructor);
-
-    void *rdata = data;
-    unsigned n = 1;
-    while (rdata == data) {
-        n <<= 1;
-        ASSERT_LT(n, 65536); // eventually the memory should be moved elsewhere
-        rdata = cxRealloc(pool->allocator, data, n * sizeof(intptr_t));
-    }
-
-    EXPECT_EQ(*((int *) rdata), 13);
-    // test if destructor is still intact
-    test_destructor_called = 0;
-    cxFree(pool->allocator, rdata);
-    EXPECT_EQ(test_destructor_called, 1);
-}
-
-
-TEST_F(CxBasicMempool, free) {
-    pool = cxBasicMempoolCreate(4);
-    auto basic_pool = reinterpret_cast<cx_basic_mempool_s *>(pool);
-
-    void *mem1;
-    void *mem2;
-
-    mem1 = cxMalloc(pool->allocator, 16);
-    cxFree(pool->allocator, mem1);
-    EXPECT_EQ(basic_pool->ndata, 0);
-
-    cxMalloc(pool->allocator, 16);
-    cxMalloc(pool->allocator, 16);
-    mem1 = cxMalloc(pool->allocator, 16);
-    cxMalloc(pool->allocator, 16);
-    mem2 = cxMalloc(pool->allocator, 16);
-
-    EXPECT_EQ(basic_pool->ndata, 5);
-    cxFree(pool->allocator, mem1);
-    EXPECT_EQ(basic_pool->ndata, 4);
-    cxFree(pool->allocator, mem2);
-    EXPECT_EQ(basic_pool->ndata, 3);
-}
\ No newline at end of file
--- a/test/test_buffer.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,815 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/buffer.h"
-
-#include <gtest/gtest.h>
-#include "util_allocator.h"
-
-class BufferFixture : public ::testing::Test {
-protected:
-    void SetUp() override {
-        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-        buf.size = 6;
-        buf.pos = 3;
-    }
-
-    void TearDown() override {
-        cxBufferDestroy(&buf);
-    }
-
-    CxBuffer buf{};
-};
-
-static void expect_default_flush_config(CxBuffer *buf) {
-    EXPECT_EQ(buf->flush_blkmax, 0);
-    EXPECT_EQ(buf->flush_blksize, 4096);
-    EXPECT_EQ(buf->flush_threshold, SIZE_MAX);
-    EXPECT_EQ(buf->flush_func, nullptr);
-    EXPECT_EQ(buf->flush_target, nullptr);
-}
-
-TEST(BufferInit, WrapSpace) {
-    CxTestingAllocator alloc;
-    CxBuffer buf;
-    void *space = cxMalloc(&alloc, 16);
-    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_DEFAULT);
-    expect_default_flush_config(&buf);
-    EXPECT_EQ(buf.space, space);
-    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
-    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, 0);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_EQ(buf.capacity, 16);
-    EXPECT_EQ(buf.allocator, &alloc);
-    cxBufferDestroy(&buf);
-    EXPECT_FALSE(alloc.verify());
-    cxFree(&alloc, space);
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(BufferInit, WrapSpaceAutoExtend) {
-    CxTestingAllocator alloc;
-    CxBuffer buf;
-    void *space = cxMalloc(&alloc, 16);
-    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_AUTO_EXTEND);
-    expect_default_flush_config(&buf);
-    EXPECT_EQ(buf.space, space);
-    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, CX_BUFFER_AUTO_EXTEND);
-    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, 0);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_EQ(buf.capacity, 16);
-    EXPECT_EQ(buf.allocator, &alloc);
-    cxBufferDestroy(&buf);
-    EXPECT_FALSE(alloc.verify());
-    cxFree(&alloc, space);
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(BufferInit, WrapSpaceAutoFree) {
-    CxTestingAllocator alloc;
-    CxBuffer buf;
-    void *space = cxMalloc(&alloc, 16);
-    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_FREE_CONTENTS);
-    expect_default_flush_config(&buf);
-    EXPECT_EQ(buf.space, space);
-    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
-    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, CX_BUFFER_FREE_CONTENTS);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_EQ(buf.capacity, 16);
-    EXPECT_EQ(buf.allocator, &alloc);
-    EXPECT_FALSE(alloc.verify());
-    cxBufferDestroy(&buf);
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(BufferInit, FreshSpace) {
-    CxTestingAllocator alloc;
-    CxBuffer buf;
-    cxBufferInit(&buf, nullptr, 8, &alloc, CX_BUFFER_DEFAULT);
-    expect_default_flush_config(&buf);
-    EXPECT_NE(buf.space, nullptr);
-    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
-    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, CX_BUFFER_FREE_CONTENTS);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(buf.allocator, &alloc);
-    EXPECT_FALSE(alloc.verify()); // space is still allocated
-    cxBufferDestroy(&buf);
-    EXPECT_TRUE(alloc.verify());
-}
-
-class BufferShiftFixture : public ::testing::Test {
-protected:
-    void SetUp() override {
-        ASSERT_TRUE(alloc.verify());
-        cxBufferInit(&buf, nullptr, 16, &alloc, CX_BUFFER_DEFAULT);
-        memcpy(buf.space, "test____________", 16);
-        buf.capacity = 8; // purposely pretend that the buffer has less capacity s.t. we can test beyond the range
-        buf.pos = 4;
-        buf.size = 4;
-    }
-
-    void TearDown() override {
-        cxBufferDestroy(&buf);
-        EXPECT_TRUE(alloc.verify());
-    }
-
-    CxTestingAllocator alloc;
-    CxBuffer buf{};
-};
-
-class BufferShiftLeft : public BufferShiftFixture {
-};
-
-TEST_F(BufferShiftLeft, Zero) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShiftLeft(&buf, 0);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 4);
-    EXPECT_EQ(buf.size, 4);
-    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
-}
-
-TEST_F(BufferShiftLeft, ZeroOffsetInterface) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShift(&buf, -0);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 4);
-    EXPECT_EQ(buf.size, 4);
-    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
-}
-
-TEST_F(BufferShiftLeft, Standard) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShiftLeft(&buf, 2);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 2);
-    EXPECT_EQ(buf.size, 2);
-    EXPECT_TRUE(memcmp(buf.space, "stst________", 8) == 0);
-}
-
-TEST_F(BufferShiftLeft, Overshift) {
-    ASSERT_LT(buf.pos, 6);
-    ASSERT_LT(buf.size, 6);
-    int ret = cxBufferShiftLeft(&buf, 6);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
-}
-
-TEST_F(BufferShiftLeft, OvershiftPosOnly) {
-    buf.pos = 2;
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShiftLeft(&buf, 3);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 1);
-    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
-}
-
-TEST_F(BufferShiftLeft, OffsetInterface) {
-    buf.pos = 3;
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShift(&buf, -2);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 1);
-    EXPECT_EQ(buf.size, 2);
-    EXPECT_TRUE(memcmp(buf.space, "stst________", 8) == 0);
-}
-
-class BufferShiftRight : public BufferShiftFixture {
-};
-
-TEST_F(BufferShiftRight, Zero) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShiftRight(&buf, 0);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 4);
-    EXPECT_EQ(buf.size, 4);
-    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
-}
-
-TEST_F(BufferShiftRight, ZeroOffsetInterface) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShift(&buf, +0);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 4);
-    EXPECT_EQ(buf.size, 4);
-    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
-}
-
-TEST_F(BufferShiftRight, Standard) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShiftRight(&buf, 3);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 7);
-    EXPECT_EQ(buf.size, 7);
-    EXPECT_TRUE(memcmp(buf.space, "testest_____", 8) == 0);
-}
-
-TEST_F(BufferShiftRight, OvershiftDiscard) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    ASSERT_EQ(buf.capacity, 8);
-    int ret = cxBufferShiftRight(&buf, 6);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 8);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_TRUE(memcmp(buf.space, "test__te____", 8) == 0);
-}
-
-TEST_F(BufferShiftRight, OvershiftExtend) {
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    ASSERT_EQ(buf.capacity, 8);
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    int ret = cxBufferShiftRight(&buf, 6);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 10);
-    EXPECT_EQ(buf.size, 10);
-    EXPECT_GE(buf.capacity, 10);
-    EXPECT_TRUE(memcmp(buf.space, "test__test__", 8) == 0);
-}
-
-TEST_F(BufferShiftRight, OffsetInterface) {
-    buf.pos = 3;
-    ASSERT_EQ(buf.size, 4);
-    int ret = cxBufferShift(&buf, 2);
-    EXPECT_EQ(ret, 0);
-    EXPECT_EQ(buf.pos, 5);
-    EXPECT_EQ(buf.size, 6);
-    EXPECT_TRUE(memcmp(buf.space, "tetest______", 8) == 0);
-}
-
-TEST(BufferMinimumCapacity, Sufficient) {
-    CxTestingAllocator alloc;
-    auto space = cxMalloc(&alloc, 8);
-    CxBuffer buf;
-    cxBufferInit(&buf, space, 8, &alloc, CX_BUFFER_FREE_CONTENTS);
-    memcpy(space, "Testing", 8);
-    buf.size = 8;
-    cxBufferMinimumCapacity(&buf, 6);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_TRUE(memcmp(buf.space, "Testing", 8) == 0);
-    cxBufferDestroy(&buf);
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(BufferMinimumCapacity, Extend) {
-    CxTestingAllocator alloc;
-    auto space = cxMalloc(&alloc, 8);
-    CxBuffer buf;
-    cxBufferInit(&buf, space, 8, &alloc, CX_BUFFER_FREE_CONTENTS); // NO auto extend!
-    memcpy(space, "Testing", 8);
-    buf.size = 8;
-    cxBufferMinimumCapacity(&buf, 16);
-    EXPECT_EQ(buf.capacity, 16);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_TRUE(memcmp(buf.space, "Testing", 8) == 0);
-    cxBufferDestroy(&buf);
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(BufferClear, Test) {
-    char space[16];
-    strcpy(space, "clear test");
-    CxBuffer buf;
-    cxBufferInit(&buf, space, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-    ASSERT_EQ(buf.size, 0);
-    // only clear the used part of the buffer
-    cxBufferClear(&buf);
-    EXPECT_EQ(memcmp(space, "clear test", 10), 0);
-    buf.size = 5;
-    buf.pos = 3;
-    cxBufferClear(&buf);
-    EXPECT_EQ(memcmp(space, "\0\0\0\0\0 test", 10), 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_EQ(buf.pos, 0);
-    cxBufferDestroy(&buf);
-}
-
-class BufferWrite : public ::testing::Test {
-protected:
-    CxBuffer buf{}, target{};
-
-    void SetUp() override {
-        cxBufferInit(&target, nullptr, 16, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-        buf.capacity = 8; // artificially reduce capacity to check OOB writes
-        memset(buf.space, 0, 16);
-        memcpy(buf.space, "prep", 4);
-        buf.size = buf.pos = 4;
-    }
-
-    void TearDown() override {
-        cxBufferDestroy(&buf);
-        cxBufferDestroy(&target);
-    }
-
-    void enableFlushing() {
-        buf.flush_target = &target;
-        buf.flush_func = reinterpret_cast<cx_write_func>(cxBufferWrite);
-        buf.flush_blkmax = 1;
-    }
-};
-
-static size_t mock_write_limited_rate(
-        void const *ptr,
-        size_t size,
-        __attribute__((unused)) size_t nitems,
-        CxBuffer *buffer
-) {
-    // simulate limited target drain capacity
-    static bool full = false;
-    if (full) {
-        full = false;
-        return 0;
-    } else {
-        full = true;
-        return cxBufferWrite(ptr, size, nitems > 2 ? 2 : nitems, buffer);
-    }
-}
-
-TEST_F(BufferWrite, SizeOneFit) {
-    const char *data = "test";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferWrite(data, 1, 4, &buf);
-    EXPECT_EQ(written, 4);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_EQ(buf.pos, 8);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
-}
-
-TEST_F(BufferWrite, SizeOneDiscard) {
-    const char *data = "testing";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferWrite(data, 1, 7, &buf);
-    EXPECT_EQ(written, 4);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_EQ(buf.pos, 8);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "preptest\0", 9), 0);
-}
-
-TEST_F(BufferWrite, SizeOneExtend) {
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    const char *data = "testing";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferWrite(data, 1, 7, &buf);
-    EXPECT_EQ(written, 7);
-    EXPECT_EQ(buf.size, 11);
-    EXPECT_EQ(buf.pos, 11);
-    EXPECT_GE(buf.capacity, 11);
-    EXPECT_EQ(memcmp(buf.space, "preptesting", 11), 0);
-}
-
-TEST_F(BufferWrite, MultibyteFit) {
-    const char *data = "test";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferWrite(data, 2, 2, &buf);
-    EXPECT_EQ(written, 2);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_EQ(buf.pos, 8);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
-}
-
-TEST_F(BufferWrite, MultibyteDiscard) {
-    const char *data = "testing";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.size, 4);
-    buf.pos = 3;
-    size_t written = cxBufferWrite(data, 2, 4, &buf);
-    // remember: whole elements are discarded if they do not fit
-    EXPECT_EQ(written, 2);
-    EXPECT_EQ(buf.size, 7);
-    EXPECT_EQ(buf.pos, 7);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "pretest\0", 8), 0);
-}
-
-TEST_F(BufferWrite, MultibyteExtend) {
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    const char *data = "tester";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.size, 4);
-    buf.pos = 3;
-    size_t written = cxBufferWrite(data, 2, 3, &buf);
-    // remember: whole elements are discarded if they do not fit
-    EXPECT_EQ(written, 3);
-    EXPECT_EQ(buf.size, 9);
-    EXPECT_EQ(buf.pos, 9);
-    EXPECT_GE(buf.capacity, 9);
-    EXPECT_EQ(memcmp(buf.space, "pretester", 9), 0);
-}
-
-TEST_F(BufferWrite, PutcWrapperFit) {
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    int c = cxBufferPut(&buf, 0x200 | 'a');
-    EXPECT_EQ(c, 'a');
-    EXPECT_EQ(buf.size, 5);
-    EXPECT_EQ(buf.pos, 5);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "prepa\0", 6), 0);
-}
-
-TEST_F(BufferWrite, PutcWrapperDiscard) {
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.size, 4);
-    buf.pos = 8;
-    int c = cxBufferPut(&buf, 0x200 | 'a');
-    EXPECT_EQ(c, EOF);
-    EXPECT_EQ(buf.size, 4);
-    EXPECT_EQ(buf.pos, 8);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "prep\0\0\0\0\0", 9), 0);
-}
-
-TEST_F(BufferWrite, PutcWrapperExtend) {
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.size, 4);
-    buf.pos = 8;
-    int c = cxBufferPut(&buf, 0x200 | 'a');
-    EXPECT_EQ(c, 'a');
-    EXPECT_EQ(buf.size, 9);
-    EXPECT_EQ(buf.pos, 9);
-    EXPECT_GE(buf.capacity, 9);
-    EXPECT_EQ(memcmp(buf.space, "prep\0\0\0\0a", 9), 0);
-}
-
-TEST_F(BufferWrite, PutStringWrapperFit) {
-    const char *data = "test";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferPutString(&buf, data);
-    EXPECT_EQ(written, 4);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_EQ(buf.pos, 8);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
-}
-
-TEST_F(BufferWrite, PutStringWrapperDiscard) {
-    const char *data = "testing";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferPutString(&buf, data);
-    EXPECT_EQ(written, 4);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_EQ(buf.pos, 8);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, "preptest\0", 9), 0);
-}
-
-TEST_F(BufferWrite, PutStringWrapperExtend) {
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    const char *data = "testing";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferPutString(&buf, data);
-    EXPECT_EQ(written, 7);
-    EXPECT_EQ(buf.size, 11);
-    EXPECT_EQ(buf.pos, 11);
-    EXPECT_GE(buf.capacity, 11);
-    EXPECT_EQ(memcmp(buf.space, "preptesting", 11), 0);
-}
-
-TEST_F(BufferWrite, MultOverflow) {
-    const char *data = "testing";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferWrite(data, 8, SIZE_MAX / 4, &buf);
-    EXPECT_EQ(written, 0);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(buf.pos, 4);
-    EXPECT_EQ(buf.size, 4);
-    EXPECT_EQ(memcmp(buf.space, "prep\0", 5), 0);
-}
-
-TEST_F(BufferWrite, MaxCapaOverflow) {
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    const char *data = "testing";
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    ASSERT_EQ(buf.size, 4);
-    size_t written = cxBufferWrite(data, 1, SIZE_MAX - 2, &buf);
-    EXPECT_EQ(written, 0);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(buf.pos, 4);
-    EXPECT_EQ(buf.size, 4);
-    EXPECT_EQ(memcmp(buf.space, "prep\0", 5), 0);
-}
-
-TEST_F(BufferWrite, OnlyOverwrite) {
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    ASSERT_EQ(buf.capacity, 8);
-    memcpy(buf.space, "preptest", 8);
-    buf.pos = 3;
-    buf.size = 8;
-    size_t written = cxBufferWrite("XXX", 2, 2, &buf);
-    EXPECT_EQ(written, 2);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(buf.size, 8);
-    EXPECT_EQ(buf.pos, 7);
-    EXPECT_EQ(memcmp(buf.space, "preXXX\0t", 8), 0);
-}
-
-TEST_F(BufferWrite, FlushAtCapacity) {
-    enableFlushing();
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    size_t written = cxBufferWrite("foo", 1, 3, &buf);
-    EXPECT_EQ(written, 3);
-    ASSERT_EQ(buf.pos, 7);
-    ASSERT_EQ(buf.size, 7);
-    ASSERT_EQ(target.pos, 0);
-    ASSERT_EQ(target.size, 0);
-    written = cxBufferWrite("hello", 1, 5, &buf);
-    EXPECT_EQ(written, 5);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(target.pos, 12);
-    ASSERT_EQ(target.size, 12);
-    EXPECT_EQ(memcmp(target.space, "prepfoohello", 12), 0);
-}
-
-TEST_F(BufferWrite, FlushAtThreshold) {
-    enableFlushing();
-    buf.flush_threshold = 12;
-    buf.flags |= CX_BUFFER_AUTO_EXTEND;
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    size_t written = cxBufferWrite("foobar", 1, 6, &buf);
-    EXPECT_EQ(written, 6);
-    ASSERT_EQ(buf.pos, 10);
-    ASSERT_EQ(buf.size, 10);
-    ASSERT_GE(buf.capacity, 10);
-    ASSERT_LE(buf.capacity, 12);
-    ASSERT_EQ(target.pos, 0);
-    ASSERT_EQ(target.size, 0);
-    written = cxBufferWrite("hello", 1, 5, &buf);
-    EXPECT_EQ(written, 5);
-    EXPECT_EQ(buf.pos, 0);
-    EXPECT_EQ(buf.size, 0);
-    EXPECT_LE(buf.capacity, 12);
-    EXPECT_EQ(target.pos, 15);
-    ASSERT_EQ(target.size, 15);
-    EXPECT_EQ(memcmp(target.space, "prepfoobarhello", 15), 0);
-}
-
-TEST_F(BufferWrite, FlushRateLimited) {
-    enableFlushing();
-    // limit the rate of the flush function and the capacity of the target
-    target.capacity = 16;
-    target.flags &= ~CX_BUFFER_AUTO_EXTEND;
-    buf.flush_func = (cx_write_func) mock_write_limited_rate;
-    ASSERT_EQ(buf.capacity, 8);
-    ASSERT_EQ(buf.pos, 4);
-    size_t written = cxBufferWrite("foo", 1, 3, &buf);
-    EXPECT_EQ(written, 3);
-    ASSERT_EQ(buf.pos, 7);
-    ASSERT_EQ(buf.size, 7);
-    ASSERT_EQ(target.pos, 0);
-    ASSERT_EQ(target.size, 0);
-    written = cxBufferWrite("hello, world!", 1, 13, &buf);
-    // " world!" fits into this buffer, the remaining stuff is flushed out
-    EXPECT_EQ(written, 13);
-    EXPECT_EQ(buf.pos, 7);
-    EXPECT_EQ(buf.size, 7);
-    EXPECT_EQ(buf.capacity, 8);
-    EXPECT_EQ(memcmp(buf.space, " world!", 7), 0);
-    EXPECT_EQ(target.pos, 13);
-    ASSERT_EQ(target.size, 13);
-    EXPECT_EQ(target.capacity, 16);
-    EXPECT_EQ(memcmp(target.space, "prepfoohello,", 13), 0);
-}
-
-class BufferSeek : public BufferFixture {
-};
-
-TEST_F(BufferSeek, SetZero) {
-    int result = cxBufferSeek(&buf, 0, SEEK_SET);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(buf.pos, 0);
-}
-
-TEST_F(BufferSeek, SetValid) {
-    int result = cxBufferSeek(&buf, 5, SEEK_SET);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(buf.pos, 5);
-}
-
-TEST_F(BufferSeek, SetInvalid) {
-    ASSERT_EQ(buf.pos, 3);
-    int result = cxBufferSeek(&buf, 6, SEEK_SET);
-    EXPECT_NE(result, 0);
-    EXPECT_EQ(buf.pos, 3);
-}
-
-TEST_F(BufferSeek, CurZero) {
-    ASSERT_EQ(buf.pos, 3);
-    int result = cxBufferSeek(&buf, 0, SEEK_CUR);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(buf.pos, 3);
-}
-
-TEST_F(BufferSeek, CurValidPositive) {
-    ASSERT_EQ(buf.pos, 3);
-    int result = cxBufferSeek(&buf, 2, SEEK_CUR);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(buf.pos, 5);
-}
-
-TEST_F(BufferSeek, CurValidNegative) {
-    ASSERT_EQ(buf.pos, 3);
-    int result = cxBufferSeek(&buf, -3, SEEK_CUR);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(buf.pos, 0);
-}
-
-TEST_F(BufferSeek, CurInvalidPositive) {
-    ASSERT_EQ(buf.pos, 3);
-    int result = cxBufferSeek(&buf, 3, SEEK_CUR);
-    EXPECT_NE(result, 0);
-    EXPECT_EQ(buf.pos, 3);
-}
-
-TEST_F(BufferSeek, CurInvalidNegative) {
-    ASSERT_EQ(buf.pos, 3);
-    int result = cxBufferSeek(&buf, -4, SEEK_CUR);
-    EXPECT_NE(result, 0);
-    EXPECT_EQ(buf.pos, 3);
-}
-
-TEST_F(BufferSeek, EndZero) {
-    ASSERT_EQ(buf.size, 6);
-    int result = cxBufferSeek(&buf, 0, SEEK_END);
-    // the (past-the-)end position is always invalid
-    EXPECT_NE(result, 0);
-    EXPECT_EQ(buf.pos, 3);
-}
-
-TEST_F(BufferSeek, EndValid) {
-    ASSERT_EQ(buf.size, 6);
-    int result = cxBufferSeek(&buf, -6, SEEK_END);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(buf.pos, 0);
-}
-
-TEST_F(BufferSeek, EndInvalid) {
-    ASSERT_EQ(buf.size, 6);
-    int result = cxBufferSeek(&buf, 1, SEEK_END);
-    EXPECT_NE(result, 0);
-    EXPECT_EQ(buf.pos, 3);
-}
-
-TEST_F(BufferSeek, WhenceInvalid) {
-    ASSERT_EQ(buf.size, 6);
-    ASSERT_EQ(buf.pos, 3);
-    int result = cxBufferSeek(&buf, 2, 9000);
-    EXPECT_NE(result, 0);
-    EXPECT_EQ(buf.size, 6);
-    EXPECT_EQ(buf.pos, 3);
-}
-
-class BufferEof : public BufferFixture {
-};
-
-TEST_F(BufferEof, Reached) {
-    buf.pos = buf.size;
-    EXPECT_TRUE(cxBufferEof(&buf));
-    buf.pos = buf.size - 1;
-    ASSERT_FALSE(cxBufferEof(&buf));
-    cxBufferPut(&buf, 'a');
-    EXPECT_TRUE(cxBufferEof(&buf));
-}
-
-TEST_F(BufferEof, NotReached) {
-    buf.pos = buf.size - 1;
-    EXPECT_FALSE(cxBufferEof(&buf));
-    buf.pos = 0;
-    cxBufferWrite("test", 1, 5, &buf);
-    EXPECT_FALSE(cxBufferEof(&buf));
-}
-
-class BufferRead : public ::testing::Test {
-protected:
-    CxBuffer buf{};
-
-    void SetUp() override {
-        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
-        buf.capacity = 8; // artificially reduce capacity to check OOB writes
-        memset(buf.space, 0, 16);
-        memcpy(buf.space, "some data", 9);
-        buf.size = 9;
-    }
-
-    void TearDown() override {
-        cxBufferDestroy(&buf);
-    }
-};
-
-TEST_F(BufferRead, GetByte) {
-    buf.pos = 2;
-    EXPECT_EQ(cxBufferGet(&buf), 'm');
-    EXPECT_EQ(cxBufferGet(&buf), 'e');
-    EXPECT_EQ(cxBufferGet(&buf), ' ');
-    EXPECT_EQ(cxBufferGet(&buf), 'd');
-    EXPECT_EQ(buf.pos, 6);
-}
-
-TEST_F(BufferRead, GetEof) {
-    buf.pos = buf.size;
-    EXPECT_EQ(cxBufferGet(&buf), EOF);
-}
-
-TEST_F(BufferRead, ReadWithinBounds) {
-    buf.pos = 2;
-    char target[4];
-    auto read = cxBufferRead(&target, 1, 4, &buf);
-    ASSERT_EQ(read, 4);
-    EXPECT_EQ(memcmp(&target, "me d", 4), 0);
-    EXPECT_EQ(buf.pos, 6);
-}
-
-TEST_F(BufferRead, ReadOutOfBounds) {
-    buf.pos = 6;
-    char target[4];
-    auto read = cxBufferRead(&target, 1, 4, &buf);
-    ASSERT_EQ(read, 3);
-    EXPECT_EQ(memcmp(&target, "ata", 3), 0);
-    EXPECT_EQ(buf.pos, 9);
-}
-
-TEST_F(BufferRead, ReadOutOfBoundsMultibyte) {
-    buf.pos = 6;
-    char target[4];
-    target[2] = '\0';
-    auto read = cxBufferRead(&target, 2, 2, &buf);
-    ASSERT_EQ(read, 1);
-    EXPECT_EQ(memcmp(&target, "at\0", 3), 0);
-    EXPECT_EQ(buf.pos, 8);
-}
-
-TEST_F(BufferRead, ReadEof) {
-    buf.pos = 9;
-    char target[4];
-    auto read = cxBufferRead(&target, 1, 1, &buf);
-    ASSERT_EQ(read, 0);
-    EXPECT_EQ(buf.pos, 9);
-}
--- a/test/test_compare.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2022 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/compare.h"
-
-#include <gtest/gtest.h>
-
-template<typename T>
-static void test_compare(
-        int (*fnc)(
-                void const *,
-                void const *
-        )
-) {
-    auto m = std::numeric_limits<T>::max() / 400;
-    T x, y;
-
-    x = (std::is_signed_v<T> ? -3 : 3) * m;
-    y = 5 * m;
-    EXPECT_LT(fnc(&x, &y), 0);
-    EXPECT_GT(fnc(&y, &x), 0);
-
-    x = 120 * m;
-    y = 348 * m;
-    EXPECT_LT(fnc(&x, &y), 0);
-    EXPECT_GT(fnc(&y, &x), 0);
-
-    if constexpr (std::is_signed_v<T>) {
-        x = -120 * m;
-        y = -348 * m;
-        EXPECT_GT(fnc(&x, &y), 0);
-        EXPECT_LT(fnc(&y, &x), 0);
-    }
-
-    x = y;
-    EXPECT_EQ(fnc(&x, &y), 0);
-    EXPECT_EQ(fnc(&y, &x), 0);
-}
-
-TEST(Compare, Int) {
-    test_compare<int>(cx_cmp_int);
-}
-
-TEST(Compare, Longint) {
-    test_compare<long int>(cx_cmp_longint);
-}
-
-TEST(Compare, Longlong) {
-    test_compare<long long>(cx_cmp_longlong);
-}
-
-TEST(Compare, Int16) {
-    test_compare<int16_t>(cx_cmp_int16);
-}
-
-TEST(Compare, Int32) {
-    test_compare<int32_t>(cx_cmp_int32);
-}
-
-TEST(Compare, Int64) {
-    test_compare<int64_t>(cx_cmp_int64);
-}
-
-TEST(Compare, Uint) {
-    test_compare<unsigned int>(cx_cmp_uint);
-}
-
-TEST(Compare, Ulongint) {
-    test_compare<unsigned long int>(cx_cmp_ulongint);
-}
-
-TEST(Compare, Ulonglong) {
-    test_compare<unsigned long long>(cx_cmp_ulonglong);
-}
-
-TEST(Compare, Uint16) {
-    test_compare<uint16_t>(cx_cmp_uint16);
-}
-
-TEST(Compare, Uint32) {
-    test_compare<uint32_t>(cx_cmp_uint32);
-}
-
-TEST(Compare, Uint64) {
-    test_compare<uint64_t>(cx_cmp_uint64);
-}
-
-TEST(Compare, Float) {
-    test_compare<float>(cx_cmp_float);
-}
-
-TEST(Compare, Double) {
-    test_compare<double>(cx_cmp_double);
-}
-
-TEST(Compare, IntPtr) {
-    test_compare<intptr_t>(cx_cmp_intptr);
-}
-
-TEST(Compare, UintPtr) {
-    test_compare<uintptr_t>(cx_cmp_uintptr);
-}
--- a/test/test_hash_key.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/hash_key.h"
-
-#include <gtest/gtest.h>
-
-TEST(cx_hash_key, functions) {
-    auto str = "my key";
-    auto len = strlen(str);
-
-    auto str_key = cx_hash_key_str(str);
-    auto bytes_key = cx_hash_key_bytes(
-            reinterpret_cast<unsigned char const *>(str), len);
-    auto obj_key = cx_hash_key(
-            reinterpret_cast<void const *>(str), len);
-
-    EXPECT_EQ(str_key.hash, bytes_key.hash);
-    EXPECT_EQ(obj_key.hash, bytes_key.hash);
-    EXPECT_EQ(str_key.len, len);
-    EXPECT_EQ(bytes_key.len, len);
-    EXPECT_EQ(bytes_key.len, len);
-    EXPECT_EQ(str_key.data.cstr, str);
-    EXPECT_EQ(bytes_key.data.cbytes, reinterpret_cast<unsigned char const *>(str));
-    EXPECT_EQ(bytes_key.data.cobj, reinterpret_cast<void const *>(str));
-}
-
-TEST(cx_hash_key, empty_string) {
-    auto str = "";
-
-    auto str_key = cx_hash_key_str(str);
-    auto bytes_key = cx_hash_key_bytes(
-            reinterpret_cast<unsigned char const *>(str), 0);
-    auto obj_key = cx_hash_key(
-            reinterpret_cast<void const *>(str), 0);
-
-    EXPECT_EQ(bytes_key.hash, 4152238450u);
-    EXPECT_EQ(str_key.hash, 4152238450u);
-    EXPECT_EQ(obj_key.hash, 4152238450u);
-    EXPECT_EQ(str_key.len, 0);
-    EXPECT_EQ(bytes_key.len, 0);
-    EXPECT_EQ(bytes_key.len, 0);
-    EXPECT_EQ(str_key.data.cstr, str);
-    EXPECT_EQ(bytes_key.data.cbytes, reinterpret_cast<unsigned char const *>(str));
-    EXPECT_EQ(bytes_key.data.cobj, reinterpret_cast<void const *>(str));
-}
-
-TEST(cx_hash_key, null_ptr) {
-    auto str_key = cx_hash_key_str(nullptr);
-    auto bytes_key = cx_hash_key_bytes(nullptr, 0);
-    auto obj_key = cx_hash_key(nullptr, 0);
-
-    EXPECT_EQ(bytes_key.hash, 1574210520u);
-    EXPECT_EQ(str_key.hash, 1574210520u);
-    EXPECT_EQ(obj_key.hash, 1574210520u);
-    EXPECT_EQ(str_key.len, 0);
-    EXPECT_EQ(bytes_key.len, 0);
-    EXPECT_EQ(bytes_key.len, 0);
-    EXPECT_EQ(str_key.data.cstr, nullptr);
-    EXPECT_EQ(bytes_key.data.cbytes, nullptr);
-    EXPECT_EQ(bytes_key.data.cobj, nullptr);
-}
--- a/test/test_list.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1077 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/linked_list.h"
-#include "cx/array_list.h"
-#include "cx/utils.h"
-#include "cx/compare.h"
-#include "util_allocator.h"
-
-#include <gtest/gtest.h>
-#include <array>
-#include <vector>
-#include <unordered_set>
-#include <algorithm>
-
-struct node {
-    node *next = nullptr;
-    node *prev = nullptr;
-    int data = 0;
-};
-
-const ptrdiff_t loc_prev = offsetof(struct node, prev);
-const ptrdiff_t loc_next = offsetof(struct node, next);
-const ptrdiff_t loc_data = offsetof(struct node, data);
-
-struct node_test_data {
-    node *begin = nullptr;
-
-    explicit node_test_data(node *begin) : begin(begin) {
-        auto n = begin;
-        while (n != nullptr) {
-            nodes.push_back(n);
-            n = n->next;
-        }
-    }
-
-    node_test_data(node_test_data &) = delete;
-
-    node_test_data(node_test_data &&) = default;
-
-    ~node_test_data() {
-        for (auto &&n: nodes) delete n;
-    }
-
-private:
-    std::vector<node *> nodes;
-};
-
-static node_test_data create_nodes_test_data(size_t len) {
-    if (len == 0) return node_test_data{nullptr};
-    auto begin = new node;
-    auto prev = begin;
-    for (size_t i = 1; i < len; i++) {
-        auto n = new node;
-        cx_linked_list_link(prev, n, loc_prev, loc_next);
-        prev = n;
-    }
-    return node_test_data{begin};
-}
-
-template<typename InputIter>
-static node_test_data create_nodes_test_data(
-        InputIter begin,
-        InputIter end
-) {
-    if (begin == end) return node_test_data{nullptr};
-    node *first = new node;
-    first->data = *begin;
-    node *prev = first;
-    begin++;
-    for (; begin != end; begin++) {
-        auto n = new node;
-        n->data = *begin;
-        cx_linked_list_link(prev, n, loc_prev, loc_next);
-        prev = n;
-    }
-    return node_test_data{first};
-}
-
-static node_test_data create_nodes_test_data(std::initializer_list<int> data) {
-    return create_nodes_test_data(data.begin(), data.end());
-}
-
-template<size_t N>
-struct int_test_data {
-    std::array<int, N> data;
-
-    int_test_data() {
-        cx_for_n (i, N) data[i] = ::rand(); // NOLINT(cert-msc50-cpp)
-    }
-};
-
-TEST(LinkedList_LowLevel, link_unlink) {
-    node a, b, c;
-
-    cx_linked_list_link(&a, &b, loc_prev, loc_next);
-    EXPECT_EQ(a.prev, nullptr);
-    EXPECT_EQ(a.next, &b);
-    EXPECT_EQ(b.prev, &a);
-    EXPECT_EQ(b.next, nullptr);
-
-    cx_linked_list_unlink(&a, &b, loc_prev, loc_next);
-    EXPECT_EQ(a.prev, nullptr);
-    EXPECT_EQ(a.next, nullptr);
-    EXPECT_EQ(b.prev, nullptr);
-    EXPECT_EQ(b.next, nullptr);
-
-    cx_linked_list_link(&b, &c, loc_prev, loc_next);
-    cx_linked_list_link(&a, &b, loc_prev, loc_next);
-    cx_linked_list_unlink(&b, &c, loc_prev, loc_next);
-    EXPECT_EQ(a.prev, nullptr);
-    EXPECT_EQ(a.next, &b);
-    EXPECT_EQ(b.prev, &a);
-    EXPECT_EQ(b.next, nullptr);
-    EXPECT_EQ(c.prev, nullptr);
-    EXPECT_EQ(c.next, nullptr);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_at) {
-    node a, b, c, d;
-    cx_linked_list_link(&a, &b, loc_prev, loc_next);
-    cx_linked_list_link(&b, &c, loc_prev, loc_next);
-    cx_linked_list_link(&c, &d, loc_prev, loc_next);
-
-    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 0), &a);
-    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 1), &b);
-    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 2), &c);
-    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 3), &d);
-    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 4), nullptr);
-
-    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_prev, 0), &a);
-    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 1), &b);
-    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 2), &c);
-    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 3), &d);
-    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 4), nullptr);
-
-    EXPECT_EQ(cx_linked_list_at(&d, 3, loc_prev, 0), &a);
-    EXPECT_EQ(cx_linked_list_at(&d, 3, loc_prev, 1), &b);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_find) {
-    auto testdata = create_nodes_test_data({2, 4, 6, 8});
-    auto list = testdata.begin;
-    int s;
-
-    s = 2;
-    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 0);
-    s = 4;
-    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 1);
-    s = 6;
-    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 2);
-    s = 8;
-    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 3);
-    s = 10;
-    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 4);
-    s = -2;
-    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 4);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_compare) {
-    auto ta = create_nodes_test_data({2, 4, 6, 8});
-    auto tb = create_nodes_test_data({2, 4, 6});
-    auto tc = create_nodes_test_data({2, 4, 6, 9});
-    auto la = ta.begin, lb = tb.begin, lc = tc.begin;
-
-    EXPECT_GT(cx_linked_list_compare(la, lb, loc_next, loc_data, cx_cmp_int), 0);
-    EXPECT_LT(cx_linked_list_compare(lb, la, loc_next, loc_data, cx_cmp_int), 0);
-    EXPECT_GT(cx_linked_list_compare(lc, la, loc_next, loc_data, cx_cmp_int), 0);
-    EXPECT_LT(cx_linked_list_compare(la, lc, loc_next, loc_data, cx_cmp_int), 0);
-    EXPECT_EQ(cx_linked_list_compare(la, la, loc_next, loc_data, cx_cmp_int), 0);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_add) {
-    // test with begin, end / prev, next
-    {
-        node nodes[4];
-        void *begin = nullptr, *end = nullptr;
-
-        cx_linked_list_add(&begin, &end, loc_prev, loc_next, &nodes[0]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[0]);
-        EXPECT_EQ(nodes[0].prev, nullptr);
-        EXPECT_EQ(nodes[0].next, nullptr);
-
-        cx_linked_list_add(&begin, &end, loc_prev, loc_next, &nodes[1]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[1]);
-        EXPECT_EQ(nodes[0].next, &nodes[1]);
-        EXPECT_EQ(nodes[1].prev, &nodes[0]);
-    }
-
-    // test with begin only / prev, next
-    {
-        node nodes[4];
-        void *begin = nullptr;
-
-        cx_linked_list_add(&begin, nullptr, loc_prev, loc_next, &nodes[0]);
-        EXPECT_EQ(begin, &nodes[0]);
-        cx_linked_list_add(&begin, nullptr, loc_prev, loc_next, &nodes[1]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(nodes[0].next, &nodes[1]);
-        EXPECT_EQ(nodes[1].prev, &nodes[0]);
-
-        cx_linked_list_add(&begin, nullptr, loc_prev, loc_next, &nodes[2]);
-        EXPECT_EQ(nodes[1].next, &nodes[2]);
-        EXPECT_EQ(nodes[2].prev, &nodes[1]);
-    }
-
-    // test with end only / prev, next
-    {
-        node nodes[4];
-        void *end = nullptr;
-
-        cx_linked_list_add(nullptr, &end, loc_prev, loc_next, &nodes[0]);
-        EXPECT_EQ(end, &nodes[0]);
-        cx_linked_list_add(nullptr, &end, loc_prev, loc_next, &nodes[1]);
-        EXPECT_EQ(end, &nodes[1]);
-        EXPECT_EQ(nodes[0].next, &nodes[1]);
-        EXPECT_EQ(nodes[1].prev, &nodes[0]);
-
-        cx_linked_list_add(nullptr, &end, loc_prev, loc_next, &nodes[2]);
-        EXPECT_EQ(end, &nodes[2]);
-        EXPECT_EQ(nodes[1].next, &nodes[2]);
-        EXPECT_EQ(nodes[2].prev, &nodes[1]);
-    }
-
-    // test with begin, end / next
-    {
-        node nodes[4];
-        void *begin = nullptr, *end = nullptr;
-
-        cx_linked_list_add(&begin, &end, -1, loc_next, &nodes[0]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[0]);
-        cx_linked_list_add(&begin, &end, -1, loc_next, &nodes[1]);
-        EXPECT_EQ(end, &nodes[1]);
-        EXPECT_EQ(nodes[0].next, &nodes[1]);
-        EXPECT_EQ(nodes[1].prev, nullptr);
-    }
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_prepend) {
-    // test with begin, end / prev, next
-    {
-        node nodes[4];
-        void *begin = nullptr, *end = nullptr;
-
-        cx_linked_list_prepend(&begin, &end, loc_prev, loc_next, &nodes[0]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[0]);
-        EXPECT_EQ(nodes[0].prev, nullptr);
-        EXPECT_EQ(nodes[0].next, nullptr);
-
-        cx_linked_list_prepend(&begin, &end, loc_prev, loc_next, &nodes[1]);
-        EXPECT_EQ(begin, &nodes[1]);
-        EXPECT_EQ(end, &nodes[0]);
-        EXPECT_EQ(nodes[1].next, &nodes[0]);
-        EXPECT_EQ(nodes[0].prev, &nodes[1]);
-    }
-
-    // test with begin only / prev, next
-    {
-        node nodes[4];
-        void *begin = nullptr;
-
-        cx_linked_list_prepend(&begin, nullptr, loc_prev, loc_next, &nodes[0]);
-        EXPECT_EQ(begin, &nodes[0]);
-        cx_linked_list_prepend(&begin, nullptr, loc_prev, loc_next, &nodes[1]);
-        EXPECT_EQ(begin, &nodes[1]);
-        EXPECT_EQ(nodes[1].next, &nodes[0]);
-        EXPECT_EQ(nodes[0].prev, &nodes[1]);
-
-        cx_linked_list_prepend(&begin, nullptr, loc_prev, loc_next, &nodes[2]);
-        EXPECT_EQ(begin, &nodes[2]);
-        EXPECT_EQ(nodes[2].next, &nodes[1]);
-        EXPECT_EQ(nodes[1].prev, &nodes[2]);
-    }
-
-    // test with end only / prev, next
-    {
-        node nodes[4];
-        void *end = nullptr;
-
-        cx_linked_list_prepend(nullptr, &end, loc_prev, loc_next, &nodes[0]);
-        EXPECT_EQ(end, &nodes[0]);
-        cx_linked_list_prepend(nullptr, &end, loc_prev, loc_next, &nodes[1]);
-        EXPECT_EQ(end, &nodes[0]);
-        EXPECT_EQ(nodes[1].next, &nodes[0]);
-        EXPECT_EQ(nodes[0].prev, &nodes[1]);
-
-        cx_linked_list_prepend(nullptr, &end, loc_prev, loc_next, &nodes[2]);
-        EXPECT_EQ(end, &nodes[0]);
-        EXPECT_EQ(nodes[2].next, &nodes[1]);
-        EXPECT_EQ(nodes[1].prev, &nodes[2]);
-    }
-
-    // test with begin, end / next
-    {
-        node nodes[4];
-        void *begin = nullptr, *end = nullptr;
-
-        cx_linked_list_prepend(&begin, &end, -1, loc_next, &nodes[0]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[0]);
-        cx_linked_list_prepend(&begin, &end, -1, loc_next, &nodes[1]);
-        cx_linked_list_prepend(&begin, &end, -1, loc_next, &nodes[2]);
-        EXPECT_EQ(begin, &nodes[2]);
-        EXPECT_EQ(end, &nodes[0]);
-        EXPECT_EQ(nodes[1].next, &nodes[0]);
-        EXPECT_EQ(nodes[2].next, &nodes[1]);
-        EXPECT_EQ(nodes[1].prev, nullptr);
-        EXPECT_EQ(nodes[0].prev, nullptr);
-    }
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_insert) {
-    // insert mid list
-    {
-        node nodes[4];
-        void *begin = &nodes[0], *end = &nodes[2];
-
-        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
-
-        cx_linked_list_insert(&begin, &end, loc_prev, loc_next, &nodes[1], &nodes[3]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[2]);
-        EXPECT_EQ(nodes[1].next, &nodes[3]);
-        EXPECT_EQ(nodes[2].prev, &nodes[3]);
-        EXPECT_EQ(nodes[3].prev, &nodes[1]);
-        EXPECT_EQ(nodes[3].next, &nodes[2]);
-    }
-
-    // insert end
-    {
-        node nodes[4];
-        void *begin = &nodes[0], *end = &nodes[2];
-
-        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
-
-        cx_linked_list_insert(&begin, &end, loc_prev, loc_next, &nodes[2], &nodes[3]);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[3]);
-        EXPECT_EQ(nodes[2].next, &nodes[3]);
-        EXPECT_EQ(nodes[3].prev, &nodes[2]);
-        EXPECT_EQ(nodes[3].next, nullptr);
-    }
-
-    // insert begin
-    {
-        node nodes[4];
-        void *begin = &nodes[0], *end = &nodes[2];
-
-        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
-
-        cx_linked_list_insert(&begin, &end, loc_prev, loc_next, nullptr, &nodes[3]);
-        EXPECT_EQ(begin, &nodes[3]);
-        EXPECT_EQ(end, &nodes[2]);
-        EXPECT_EQ(nodes[0].prev, &nodes[3]);
-        EXPECT_EQ(nodes[3].prev, nullptr);
-        EXPECT_EQ(nodes[3].next, &nodes[0]);
-    }
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_insert_chain) {
-    // insert mid list
-    {
-        node nodes[5];
-        void *begin = &nodes[0], *end = &nodes[2];
-
-        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[3], &nodes[4], loc_prev, loc_next);
-
-        cx_linked_list_insert_chain(&begin, &end, loc_prev, loc_next, &nodes[1], &nodes[3], nullptr);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[2]);
-        EXPECT_EQ(nodes[1].next, &nodes[3]);
-        EXPECT_EQ(nodes[2].prev, &nodes[4]);
-        EXPECT_EQ(nodes[3].prev, &nodes[1]);
-        EXPECT_EQ(nodes[4].next, &nodes[2]);
-    }
-
-    // insert end
-    {
-        node nodes[5];
-        void *begin = &nodes[0], *end = &nodes[2];
-
-        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[3], &nodes[4], loc_prev, loc_next);
-
-        cx_linked_list_insert_chain(&begin, &end, loc_prev, loc_next, &nodes[2], &nodes[3], nullptr);
-        EXPECT_EQ(begin, &nodes[0]);
-        EXPECT_EQ(end, &nodes[4]);
-        EXPECT_EQ(nodes[2].next, &nodes[3]);
-        EXPECT_EQ(nodes[3].prev, &nodes[2]);
-        EXPECT_EQ(nodes[4].next, nullptr);
-    }
-
-    // insert begin
-    {
-        node nodes[5];
-        void *begin = &nodes[0], *end = &nodes[2];
-
-        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
-        cx_linked_list_link(&nodes[3], &nodes[4], loc_prev, loc_next);
-
-        cx_linked_list_insert_chain(&begin, &end, loc_prev, loc_next, nullptr, &nodes[3], nullptr);
-        EXPECT_EQ(begin, &nodes[3]);
-        EXPECT_EQ(end, &nodes[2]);
-        EXPECT_EQ(nodes[0].prev, &nodes[4]);
-        EXPECT_EQ(nodes[3].prev, nullptr);
-        EXPECT_EQ(nodes[4].next, &nodes[0]);
-    }
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_first) {
-    auto testdata = create_nodes_test_data(3);
-    auto begin = testdata.begin;
-    EXPECT_EQ(cx_linked_list_first(begin, loc_prev), begin);
-    EXPECT_EQ(cx_linked_list_first(begin->next, loc_prev), begin);
-    EXPECT_EQ(cx_linked_list_first(begin->next->next, loc_prev), begin);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_last) {
-    auto testdata = create_nodes_test_data(3);
-    auto begin = testdata.begin;
-    auto end = begin->next->next;
-    EXPECT_EQ(cx_linked_list_last(begin, loc_next), end);
-    EXPECT_EQ(cx_linked_list_last(begin->next, loc_next), end);
-    EXPECT_EQ(cx_linked_list_last(begin->next->next, loc_next), end);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_prev) {
-    auto testdata = create_nodes_test_data(3);
-    auto begin = testdata.begin;
-    EXPECT_EQ(cx_linked_list_prev(begin, loc_next, begin), nullptr);
-    EXPECT_EQ(cx_linked_list_prev(begin, loc_next, begin->next), begin);
-    EXPECT_EQ(cx_linked_list_prev(begin, loc_next, begin->next->next), begin->next);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_remove) {
-    auto testdata = create_nodes_test_data({2, 4, 6});
-    auto begin = reinterpret_cast<void *>(testdata.begin);
-    auto first = testdata.begin;
-    auto second = first->next;
-    auto third = second->next;
-    auto end = reinterpret_cast<void *>(third);
-
-    cx_linked_list_remove(&begin, &end, loc_prev, loc_next, second);
-    EXPECT_EQ(begin, first);
-    EXPECT_EQ(end, third);
-    EXPECT_EQ(first->prev, nullptr);
-    EXPECT_EQ(first->next, third);
-    EXPECT_EQ(third->prev, first);
-    EXPECT_EQ(third->next, nullptr);
-
-    cx_linked_list_remove(&begin, &end, loc_prev, loc_next, third);
-    EXPECT_EQ(begin, first);
-    EXPECT_EQ(end, first);
-    EXPECT_EQ(first->prev, nullptr);
-    EXPECT_EQ(first->next, nullptr);
-
-    cx_linked_list_remove(&begin, &end, loc_prev, loc_next, first);
-    EXPECT_EQ(begin, nullptr);
-    EXPECT_EQ(end, nullptr);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_size) {
-    EXPECT_EQ(cx_linked_list_size(nullptr, loc_next), 0);
-
-    {
-        auto testdata = create_nodes_test_data(5);
-        EXPECT_EQ(cx_linked_list_size(testdata.begin, loc_next), 5);
-    }
-
-    {
-        auto testdata = create_nodes_test_data(13);
-        EXPECT_EQ(cx_linked_list_size(testdata.begin, loc_next), 13);
-    }
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_sort) {
-    int_test_data<1500> testdata;
-    std::array<int, 1500> sorted{};
-    std::partial_sort_copy(testdata.data.begin(), testdata.data.end(), sorted.begin(), sorted.end());
-
-    auto scrambled = create_nodes_test_data(testdata.data.begin(), testdata.data.end());
-    void *begin = scrambled.begin;
-    void *end = cx_linked_list_last(begin, loc_next);
-
-    cx_linked_list_sort(&begin, &end, loc_prev, loc_next, loc_data, cx_cmp_int);
-
-    node *check = reinterpret_cast<node *>(begin);
-    node *check_last = nullptr;
-    cx_for_n (i, sorted.size()) {
-        EXPECT_EQ(check->data, sorted[i]);
-        EXPECT_EQ(check->prev, check_last);
-        if (i < sorted.size() - 1) {
-            ASSERT_NE(check->next, nullptr);
-        }
-        check_last = check;
-        check = check->next;
-    }
-    EXPECT_EQ(check, nullptr);
-    EXPECT_EQ(end, check_last);
-}
-
-TEST(LinkedList_LowLevel, cx_linked_list_reverse) {
-    auto testdata = create_nodes_test_data({2, 4, 6, 8});
-    auto expected = create_nodes_test_data({8, 6, 4, 2});
-
-    auto begin = reinterpret_cast<void *>(testdata.begin);
-    auto end = cx_linked_list_last(begin, loc_next);
-    auto orig_begin = begin, orig_end = end;
-
-    cx_linked_list_reverse(&begin, &end, loc_prev, loc_next);
-    EXPECT_EQ(end, orig_begin);
-    EXPECT_EQ(begin, orig_end);
-    EXPECT_EQ(cx_linked_list_compare(begin, expected.begin, loc_next, loc_data, cx_cmp_int), 0);
-}
-
-class HighLevelTest : public ::testing::Test {
-    mutable std::unordered_set<CxList *> lists;
-protected:
-    CxTestingAllocator testingAllocator;
-
-    void TearDown() override {
-        for (auto &&l: lists) cxListDestroy(l);
-        EXPECT_TRUE(testingAllocator.verify());
-    }
-
-    static constexpr size_t testdata_len = 250;
-    int_test_data<testdata_len> testdata;
-
-    auto autofree(CxList *list) const -> CxList * {
-        if (list != nullptr) lists.insert(list);
-        return list;
-    }
-
-    auto linkedListFromTestData() const -> CxList * {
-        auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
-        cxListAddArray(list, testdata.data.data(), testdata_len);
-        return list;
-    }
-
-    auto pointerLinkedListFromTestData() const -> CxList * {
-        auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
-        cxListStorePointers(list);
-        // note: cannot use cxListAddArray() because we don't have a list of pointers
-        cx_for_n(i, testdata_len) cxListAdd(list, &testdata.data[i]);
-        return list;
-    }
-
-    auto arrayListFromTestData() const -> CxList * {
-        auto list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), testdata_len));
-        cxListAddArray(list, testdata.data.data(), testdata_len);
-        return list;
-    }
-
-    void verifyCreate(CxList *list) const {
-        EXPECT_EQ(list->content_destructor_type, CX_DESTRUCTOR_NONE);
-        EXPECT_EQ(list->size, 0);
-        EXPECT_EQ(list->allocator, &testingAllocator);
-        EXPECT_EQ(list->cmpfunc, cx_cmp_int);
-    }
-
-    void verifyAdd(
-            CxList *list,
-            bool as_pointer
-    ) {
-        auto len = testdata_len;
-        cx_for_n (i, len) EXPECT_EQ(cxListAdd(list, &testdata.data[i]), 0);
-        EXPECT_EQ(list->size, len);
-        EXPECT_GE(list->capacity, list->size);
-        cx_for_n (i, len) EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i]);
-        cx_for_n (i, len) ++testdata.data[i];
-        if (as_pointer) {
-            cx_for_n (i, len) EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i]);
-        } else {
-            cx_for_n (i, len) EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i] - 1);
-        }
-    }
-
-    static void verifyInsert(CxList *list) {
-        int a = 5, b = 47, c = 13, d = 42;
-
-        EXPECT_NE(cxListInsert(list, 1, &a), 0);
-        EXPECT_EQ(list->size, 0);
-        EXPECT_EQ(cxListInsert(list, 0, &a), 0);
-        EXPECT_EQ(list->size, 1);
-        EXPECT_EQ(cxListInsert(list, 0, &b), 0);
-        EXPECT_EQ(list->size, 2);
-        EXPECT_EQ(cxListInsert(list, 1, &c), 0);
-        EXPECT_EQ(list->size, 3);
-        EXPECT_EQ(cxListInsert(list, 3, &d), 0);
-
-        ASSERT_EQ(list->size, 4);
-        EXPECT_GE(list->capacity, list->size);
-
-        EXPECT_EQ(*(int *) cxListAt(list, 0), 47);
-        EXPECT_EQ(*(int *) cxListAt(list, 1), 13);
-        EXPECT_EQ(*(int *) cxListAt(list, 2), 5);
-        EXPECT_EQ(*(int *) cxListAt(list, 3), 42);
-    }
-
-    static void verifyInsertArray(
-            CxList *list,
-            bool pointers = false
-    ) {
-        int a[5] = {5, 47, 11, 13, 42};
-        int b[5] = {9, 18, 72, 50, 7};
-        int *aptr[5];
-        int *bptr[5];
-        cx_for_n(i, 5) {
-            aptr[i] = &a[i];
-            bptr[i] = &b[i];
-        }
-
-        size_t inserted;
-
-        if (pointers) {
-            inserted = cxListInsertArray(list, 0, aptr, 5);
-        } else {
-            inserted = cxListInsertArray(list, 0, a, 5);
-        }
-        EXPECT_EQ(inserted, 5);
-        EXPECT_EQ(*(int *) cxListAt(list, 0), 5);
-        EXPECT_EQ(*(int *) cxListAt(list, 1), 47);
-        EXPECT_EQ(*(int *) cxListAt(list, 2), 11);
-        EXPECT_EQ(*(int *) cxListAt(list, 3), 13);
-        EXPECT_EQ(*(int *) cxListAt(list, 4), 42);
-        if (pointers) {
-            inserted = cxListInsertArray(list, 3, bptr, 5);
-        } else {
-            inserted = cxListInsertArray(list, 3, b, 5);
-        }
-        EXPECT_EQ(inserted, 5);
-        EXPECT_EQ(*(int *) cxListAt(list, 0), 5);
-        EXPECT_EQ(*(int *) cxListAt(list, 1), 47);
-        EXPECT_EQ(*(int *) cxListAt(list, 2), 11);
-        EXPECT_EQ(*(int *) cxListAt(list, 3), 9);
-        EXPECT_EQ(*(int *) cxListAt(list, 4), 18);
-        EXPECT_EQ(*(int *) cxListAt(list, 5), 72);
-        EXPECT_EQ(*(int *) cxListAt(list, 6), 50);
-        EXPECT_EQ(*(int *) cxListAt(list, 7), 7);
-        EXPECT_EQ(*(int *) cxListAt(list, 8), 13);
-        EXPECT_EQ(*(int *) cxListAt(list, 9), 42);
-    }
-
-    void verifyRemove(CxList *list) const {
-        EXPECT_EQ(cxListRemove(list, 2), 0);
-        EXPECT_EQ(cxListRemove(list, 4), 0);
-        EXPECT_EQ(list->size, testdata_len - 2);
-        EXPECT_GE(list->capacity, list->size);
-        EXPECT_EQ(*(int *) cxListAt(list, 0), testdata.data[0]);
-        EXPECT_EQ(*(int *) cxListAt(list, 1), testdata.data[1]);
-        EXPECT_EQ(*(int *) cxListAt(list, 2), testdata.data[3]);
-        EXPECT_EQ(*(int *) cxListAt(list, 3), testdata.data[4]);
-        EXPECT_EQ(*(int *) cxListAt(list, 4), testdata.data[6]);
-
-        EXPECT_EQ(cxListRemove(list, 0), 0);
-        EXPECT_EQ(list->size, testdata_len - 3);
-        EXPECT_GE(list->capacity, list->size);
-        EXPECT_EQ(*(int *) cxListAt(list, 0), testdata.data[1]);
-        EXPECT_EQ(*(int *) cxListAt(list, 1), testdata.data[3]);
-
-        EXPECT_NE(cxListRemove(list, testdata_len), 0);
-    }
-
-    void verifyAt(CxList *list) const {
-        auto len = testdata_len;
-        EXPECT_EQ(list->size, len);
-        cx_for_n (i, len) {
-            EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i]);
-        }
-        EXPECT_EQ(cxListAt(list, list->size), nullptr);
-    }
-
-    void verifyFind(CxList *list) const {
-        cx_for_n (attempt, 25) {
-            size_t exp = rand() % testdata_len; // NOLINT(cert-msc50-cpp)
-            int val = testdata.data[exp];
-            // randomly picked number could occur earlier in list - find first position
-            cx_for_n (i, exp) {
-                if (testdata.data[i] == val) {
-                    exp = i;
-                    break;
-                }
-            }
-            EXPECT_EQ(cxListFind(list, &val), exp);
-        }
-    }
-
-    void verifySort(CxList *list) const {
-        std::array<int, testdata_len> expected{};
-        std::partial_sort_copy(testdata.data.begin(), testdata.data.end(), expected.begin(), expected.end());
-        cxListSort(list);
-        cx_for_n (i, testdata_len) ASSERT_EQ(*(int *) cxListAt(list, i), expected[i]);
-    }
-
-    void verifyIterator(CxList *list) const {
-        int i = 0;
-        auto iter = cxListBeginMut(list);
-        cx_foreach(int*, x, iter) {
-            ASSERT_EQ(iter.index, (size_t) (i + 1) / 2);
-            ASSERT_EQ(*x, testdata.data[i]);
-            if (i % 2 == 1) cxIteratorFlagRemoval(iter);
-            i++;
-        }
-        auto len = testdata_len;
-        EXPECT_EQ(i, len);
-        ASSERT_EQ(list->size, len / 2);
-        cx_for_n(j, len / 2) ASSERT_EQ(*(int *) cxListAt(list, j), testdata.data[j * 2]);
-    }
-
-    static void verifyInsertViaIterator(CxList *list) {
-        int newdata[] = {10, 20, 30, 40, 50};
-
-        auto iter = cxListMutIterator(list, 2);
-        EXPECT_TRUE(cxIteratorValid(iter));
-        EXPECT_EQ(iter.index, 2);
-        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 2);
-        cxListInsertAfter(&iter, &newdata[0]);
-        EXPECT_TRUE(cxIteratorValid(iter));
-        EXPECT_EQ(iter.index, 2);
-        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 2);
-        cxListInsertBefore(&iter, &newdata[1]);
-        EXPECT_TRUE(cxIteratorValid(iter));
-        EXPECT_EQ(iter.index, 3);
-        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 2);
-
-        iter = cxListBeginMut(list);
-        cxListInsertBefore(&iter, &newdata[2]);
-        EXPECT_TRUE(cxIteratorValid(iter));
-        EXPECT_EQ(iter.index, 1);
-        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 0);
-        iter = cxListMutIterator(list, list->size);
-        cxListInsertBefore(&iter, &newdata[3]);
-        EXPECT_FALSE(cxIteratorValid(iter));
-        EXPECT_EQ(iter.index, 9);
-        iter = cxListMutIterator(list, list->size);
-        cxListInsertAfter(&iter, &newdata[4]);
-        EXPECT_FALSE(cxIteratorValid(iter));
-        EXPECT_EQ(iter.index, 10);
-
-        int expdata[] = {30, 0, 1, 20, 2, 10, 3, 4, 40, 50};
-        cx_for_n (j, 10) EXPECT_EQ(*(int *) cxListAt(list, j), expdata[j]);
-    }
-
-    void verifyReverse(CxList *list) const {
-        cxListReverse(list);
-        cx_for_n(i, testdata_len) {
-            ASSERT_EQ(*(int *) cxListAt(list, i), testdata.data[testdata_len - 1 - i]);
-        }
-    }
-
-    static void verifyCompare(
-            CxList *left,
-            CxList *right
-    ) {
-        EXPECT_EQ(cxListCompare(left, right), 0);
-        int x = 42;
-        cxListAdd(left, &x);
-        ASSERT_GT(left->size, right->size);
-        EXPECT_GT(cxListCompare(left, right), 0);
-        EXPECT_LT(cxListCompare(right, left), 0);
-        cxListAdd(right, &x);
-        ASSERT_EQ(left->size, right->size);
-        EXPECT_EQ(cxListCompare(left, right), 0);
-        int a = 5, b = 10;
-        cxListInsert(left, 15, &a);
-        cxListInsert(right, 15, &b);
-        ASSERT_EQ(left->size, right->size);
-        EXPECT_LT(cxListCompare(left, right), 0);
-        EXPECT_GT(cxListCompare(right, left), 0);
-        *(int *) cxListAt(left, 15) = 10;
-        EXPECT_EQ(cxListCompare(left, right), 0);
-    }
-};
-
-class LinkedList : public HighLevelTest {
-};
-
-class PointerLinkedList : public HighLevelTest {
-};
-
-class ArrayList : public HighLevelTest {
-};
-
-TEST_F(PointerLinkedList, cxListStorePointers) {
-    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, 47));
-    EXPECT_FALSE(cxListIsStoringPointers(list));
-    cxListStorePointers(list);
-    EXPECT_EQ(list->itemsize, sizeof(void *));
-    EXPECT_NE(list->cl, nullptr);
-    EXPECT_NE(list->climpl, nullptr);
-    EXPECT_TRUE(cxListIsStoringPointers(list));
-    cxListStoreObjects(list);
-    EXPECT_NE(list->cl, nullptr);
-    EXPECT_EQ(list->climpl, nullptr);
-    EXPECT_FALSE(cxListIsStoringPointers(list));
-}
-
-TEST_F(LinkedList, cxLinkedListCreate) {
-    CxList *list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
-    ASSERT_NE(list, nullptr);
-    EXPECT_EQ(list->itemsize, sizeof(int));
-    EXPECT_EQ(list->capacity, (size_t) -1);
-    verifyCreate(list);
-}
-
-TEST_F(ArrayList, cxArrayListCreate) {
-    CxList *list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 8));
-    ASSERT_NE(list, nullptr);
-    EXPECT_EQ(list->itemsize, sizeof(int));
-    EXPECT_EQ(list->capacity, 8);
-    verifyCreate(list);
-}
-
-TEST_F(LinkedList, cxListAdd) {
-    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
-    verifyAdd(list, false);
-}
-
-TEST_F(PointerLinkedList, cxListAdd) {
-    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
-    cxListStorePointers(list);
-    verifyAdd(list, true);
-}
-
-TEST_F(ArrayList, cxListAdd) {
-    auto list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 8));
-    verifyAdd(list, false);
-}
-
-TEST_F(LinkedList, cxListInsert) {
-    verifyInsert(autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int))));
-}
-
-TEST_F(PointerLinkedList, cxListInsert) {
-    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
-    cxListStorePointers(list);
-    verifyInsert(list);
-}
-
-TEST_F(ArrayList, cxListInsert) {
-    verifyInsert(autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 2)));
-}
-
-TEST_F(LinkedList, cxListInsertArray) {
-    verifyInsertArray(autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int))));
-}
-
-TEST_F(PointerLinkedList, cxListInsertArray) {
-    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
-    cxListStorePointers(list);
-    verifyInsertArray(list, true);
-}
-
-TEST_F(ArrayList, cxListInsertArray) {
-    verifyInsertArray(autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 4)));
-}
-
-TEST_F(LinkedList, cxListRemove) {
-    verifyRemove(linkedListFromTestData());
-}
-
-TEST_F(PointerLinkedList, cxListRemove) {
-    verifyRemove(pointerLinkedListFromTestData());
-}
-
-TEST_F(ArrayList, cxListRemove) {
-    verifyRemove(arrayListFromTestData());
-}
-
-TEST_F(LinkedList, cxListAt) {
-    verifyAt(linkedListFromTestData());
-}
-
-TEST_F(PointerLinkedList, cxListAt) {
-    verifyAt(pointerLinkedListFromTestData());
-}
-
-TEST_F(ArrayList, cxListAt) {
-    verifyAt(arrayListFromTestData());
-}
-
-TEST_F(LinkedList, cxListFind) {
-    verifyFind(linkedListFromTestData());
-}
-
-TEST_F(PointerLinkedList, cxListFind) {
-    verifyFind(pointerLinkedListFromTestData());
-}
-
-TEST_F(ArrayList, cxListFind) {
-    verifyFind(arrayListFromTestData());
-}
-
-TEST_F(LinkedList, cxListSort) {
-    verifySort(linkedListFromTestData());
-}
-
-TEST_F(PointerLinkedList, cxListSort) {
-    verifySort(pointerLinkedListFromTestData());
-}
-
-TEST_F(ArrayList, cxListSort) {
-    verifySort(arrayListFromTestData());
-}
-
-TEST_F(LinkedList, Iterator) {
-    verifyIterator(linkedListFromTestData());
-}
-
-TEST_F(PointerLinkedList, Iterator) {
-    verifyIterator(pointerLinkedListFromTestData());
-}
-
-TEST_F(ArrayList, Iterator) {
-    verifyIterator(arrayListFromTestData());
-}
-
-TEST_F(LinkedList, InsertViaIterator) {
-    int fivenums[] = {0, 1, 2, 3, 4, 5};
-    CxList *list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
-    cxListAddArray(list, fivenums, 5);
-    verifyInsertViaIterator(list);
-}
-
-TEST_F(PointerLinkedList, InsertViaIterator) {
-    int fivenums[] = {0, 1, 2, 3, 4, 5};
-    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
-    cxListStorePointers(list);
-    // note: cannot use cxListAddArray() because we don't have a list of pointers
-    cx_for_n(i, 5) cxListAdd(list, &fivenums[i]);
-    verifyInsertViaIterator(list);
-}
-
-TEST_F(ArrayList, InsertViaIterator) {
-    int fivenums[] = {0, 1, 2, 3, 4, 5};
-    CxList *list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 4));
-    cxListAddArray(list, fivenums, 5);
-    verifyInsertViaIterator(list);
-}
-
-TEST_F(LinkedList, cxListReverse) {
-    verifyReverse(linkedListFromTestData());
-}
-
-TEST_F(PointerLinkedList, cxListReverse) {
-    verifyReverse(pointerLinkedListFromTestData());
-}
-
-TEST_F(ArrayList, cxListReverse) {
-    verifyReverse(arrayListFromTestData());
-}
-
-TEST_F(LinkedList, cxListCompare) {
-    auto left = linkedListFromTestData();
-    auto right = linkedListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(LinkedList, cxListCompareWithPtrList) {
-    auto left = linkedListFromTestData();
-    auto right = pointerLinkedListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(LinkedList, cxListCompareWithArrayList) {
-    auto left = linkedListFromTestData();
-    auto right = arrayListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(PointerLinkedList, cxListCompare) {
-    auto left = pointerLinkedListFromTestData();
-    auto right = pointerLinkedListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(PointerLinkedList, cxListCompareWithNormalList) {
-    auto left = pointerLinkedListFromTestData();
-    auto right = linkedListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(PointerLinkedList, cxListCompareWithArrayList) {
-    auto left = pointerLinkedListFromTestData();
-    auto right = arrayListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(ArrayList, cxListCompare) {
-    auto left = arrayListFromTestData();
-    auto right = arrayListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(ArrayList, cxListCompareWithPtrList) {
-    auto left = arrayListFromTestData();
-    auto right = pointerLinkedListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(ArrayList, cxListCompareWithNormalList) {
-    auto left = arrayListFromTestData();
-    auto right = linkedListFromTestData();
-    verifyCompare(left, right);
-}
-
-TEST_F(PointerLinkedList, NoDestructor) {
-    void *item = cxMalloc(&testingAllocator, sizeof(int));
-    auto list = cxLinkedListCreate(cxDefaultAllocator, cx_cmp_int, sizeof(int *));
-    cxListStorePointers(list);
-    cxListAdd(list, item);
-    ASSERT_FALSE(testingAllocator.verify());
-    cxListDestroy(list);
-    EXPECT_FALSE(testingAllocator.verify());
-    cxFree(&testingAllocator, item);
-    EXPECT_TRUE(testingAllocator.verify());
-}
-
-TEST_F(PointerLinkedList, SimpleDestructor) {
-    int item = 0;
-    auto list = cxLinkedListCreate(cxDefaultAllocator, cx_cmp_int, sizeof(int *));
-    cxListStorePointers(list);
-    list->content_destructor_type = CX_DESTRUCTOR_SIMPLE;
-    list->simple_destructor = [](void *elem) { *(int *) elem = 42; };
-    cxListAdd(list, &item);
-    cxListDestroy(list);
-    EXPECT_EQ(item, 42);
-}
-
-TEST_F(PointerLinkedList, AdvancedDestructor) {
-    void *item = cxMalloc(&testingAllocator, sizeof(int));
-    auto list = cxLinkedListCreate(cxDefaultAllocator, cx_cmp_int, sizeof(int *));
-    cxListStorePointers(list);
-    list->content_destructor_type = CX_DESTRUCTOR_ADVANCED;
-    list->advanced_destructor.data = &testingAllocator;
-    list->advanced_destructor.func = (cx_destructor_func2) cxFree;
-    cxListAdd(list, item);
-    ASSERT_FALSE(testingAllocator.verify());
-    cxListDestroy(list);
-    EXPECT_TRUE(testingAllocator.verify());
-}
--- a/test/test_map.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,272 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/hash_map.h"
-#include "cx/utils.h"
-#include "util_allocator.h"
-
-#include <gtest/gtest.h>
-#include <unordered_map>
-#include <unordered_set>
-
-struct map_operation {
-    enum {
-        put, rm
-    } op;
-    char const *key;
-    char const *value;
-};
-
-auto generate_map_operations() -> std::vector<map_operation> {
-    return {
-            {map_operation::put, "key 1",          "test"},
-            {map_operation::put, "key 2",          "blub"},
-            {map_operation::put, "key 3",          "hallo"},
-            {map_operation::put, "key 2",          "foobar"},
-            {map_operation::put, "key 4",          "value 4"},
-            {map_operation::put, "key 5",          "value 5"},
-            {map_operation::put, "key 6",          "value 6"},
-            {map_operation::rm,  "key 4",          nullptr},
-            {map_operation::put, "key 7",          "value 7"},
-            {map_operation::put, "key 8",          "value 8"},
-            {map_operation::rm,  "does not exist", nullptr},
-            {map_operation::put, "key 9",          "value 9"},
-            {map_operation::put, "key 6",          "other value"},
-            {map_operation::put, "key 7",          "something else"},
-            {map_operation::rm,  "key 8",          nullptr},
-            {map_operation::rm,  "key 2",          nullptr},
-            {map_operation::put, "key 8",          "new value"},
-    };
-}
-
-static void verify_map_contents(
-        CxMap *map,
-        std::unordered_map<std::string, std::string> const &refmap
-) {
-    // verify key iterator
-    {
-        auto keyiter = cxMapIteratorKeys(map);
-        std::unordered_set<std::string> keys;
-        cx_foreach(CxHashKey*, elem, keyiter) {
-            keys.insert(std::string(elem->data.cstr, elem->len));
-        }
-        EXPECT_EQ(keyiter.index, map->size);
-        ASSERT_EQ(keys.size(), map->size);
-        for (auto &&k: keys) {
-            EXPECT_NE(refmap.find(k), refmap.end());
-        }
-    }
-
-    // verify value iterator
-    {
-        auto valiter = cxMapIteratorValues(map);
-        std::unordered_set<std::string> values; // we use that the values in our test data are unique strings
-        cx_foreach(char const*, elem, valiter) {
-            values.insert(std::string(elem));
-        }
-        EXPECT_EQ(valiter.index, map->size);
-        ASSERT_EQ(values.size(), map->size);
-        for (auto &&v: values) {
-            EXPECT_NE(std::find_if(refmap.begin(), refmap.end(),
-                                   [v](auto const &e) { return e.second == v; }), refmap.end());
-        }
-    }
-
-    // verify pair iterator
-    {
-        auto pairiter = cxMapIterator(map);
-        std::unordered_map<std::string, std::string> pairs;
-        cx_foreach(CxMapEntry*, entry, pairiter) {
-            pairs[std::string(entry->key->data.cstr, entry->key->len)] = std::string((char *) entry->value);
-        }
-        EXPECT_EQ(pairiter.index, map->size);
-        ASSERT_EQ(pairs.size(), refmap.size());
-        for (auto &&p: pairs) {
-            ASSERT_EQ(p.second, refmap.at(p.first));
-        }
-    }
-}
-
-TEST(CxHashMap, Create) {
-    CxTestingAllocator allocator;
-    auto map = cxHashMapCreate(&allocator, 0);
-    auto hmap = reinterpret_cast<struct cx_hash_map_s *>(map);
-    EXPECT_GT(hmap->bucket_count, 0);
-    cx_for_n(i, hmap->bucket_count) {
-        EXPECT_EQ(hmap->buckets[i], nullptr);
-    }
-    EXPECT_EQ(map->size, 0);
-    EXPECT_EQ(map->allocator, &allocator);
-
-    cxMapDestroy(map);
-    EXPECT_TRUE(allocator.verify());
-}
-
-TEST(CxHashMap, BasicOperations) {
-    // create the map
-    CxTestingAllocator allocator;
-    auto map = cxHashMapCreate(&allocator, 8);
-
-    // create a reference map
-    std::unordered_map<std::string, std::string> refmap;
-
-    // generate operations
-    auto ops = generate_map_operations();
-
-    // verify iterators for empty map
-    verify_map_contents(map, refmap);
-
-    // execute operations and verify results
-    for (auto &&op: ops) {
-        CxHashKey key = cx_hash_key_str(op.key);
-        key.hash = 0; // force the hash map to compute the hash
-        if (op.op == map_operation::put) {
-            // execute a put operation and verify that the exact value can be read back
-            refmap[std::string(op.key)] = std::string(op.value);
-            int result = cxMapPut(map, key, (void *) op.value);
-            EXPECT_EQ(result, 0);
-            auto added = cxMapGet(map, key);
-            EXPECT_EQ(memcmp(op.value, added, strlen(op.value)), 0);
-        } else {
-            // execute a remove and verify that the removed element was returned (or nullptr)
-            auto found = refmap.find(op.key);
-            auto removed = cxMapRemove(map, key);
-            if (found == refmap.end()) {
-                EXPECT_EQ(removed, nullptr);
-            } else {
-                EXPECT_EQ(std::string((char *) removed), found->second);
-                refmap.erase(found);
-            }
-        }
-        // compare the current map state with the reference map
-        verify_map_contents(map, refmap);
-    }
-
-    // destroy the map and verify the memory (de)allocations
-    cxMapDestroy(map);
-    EXPECT_TRUE(allocator.verify());
-}
-
-TEST(CxHashMap, RemoveViaIterator) {
-    CxTestingAllocator allocator;
-    auto map = cxHashMapCreate(&allocator, 4);
-
-    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
-    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
-    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
-    cxMapPut(map, cx_hash_key_str("key 4"), (void *) "val 4");
-    cxMapPut(map, cx_hash_key_str("key 5"), (void *) "val 5");
-    cxMapPut(map, cx_hash_key_str("key 6"), (void *) "val 6");
-
-    auto iter = cxMapMutIterator(map);
-    cx_foreach(CxMapEntry*, entry, iter) {
-        if (entry->key->data.cstr[4] % 2 == 1) cxIteratorFlagRemoval(iter);
-    }
-    EXPECT_EQ(map->size, 3);
-    EXPECT_EQ(iter.index, map->size);
-
-    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 1")), nullptr);
-    EXPECT_NE(cxMapGet(map, cx_hash_key_str("key 2")), nullptr);
-    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 3")), nullptr);
-    EXPECT_NE(cxMapGet(map, cx_hash_key_str("key 4")), nullptr);
-    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 5")), nullptr);
-    EXPECT_NE(cxMapGet(map, cx_hash_key_str("key 6")), nullptr);
-
-    cxMapDestroy(map);
-    EXPECT_TRUE(allocator.verify());
-}
-
-TEST(CxHashMap, RehashNotRequired) {
-    CxTestingAllocator allocator;
-    auto map = cxHashMapCreate(&allocator, 8);
-
-    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
-    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
-    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
-    cxMapPut(map, cx_hash_key_str("key 4"), (void *) "val 4");
-    cxMapPut(map, cx_hash_key_str("key 5"), (void *) "val 5");
-    cxMapPut(map, cx_hash_key_str("key 6"), (void *) "val 6");
-
-    // 6/8 does not exceed 0.75, therefore the function should not rehash
-    int result = cxMapRehash(map);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(reinterpret_cast<struct cx_hash_map_s *>(map)->bucket_count, 8);
-
-    cxMapDestroy(map);
-    EXPECT_TRUE(allocator.verify());
-}
-
-TEST(CxHashMap, Rehash) {
-    CxTestingAllocator allocator;
-    auto map = cxHashMapCreate(&allocator, 8);
-
-    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
-    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
-    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
-    cxMapPut(map, cx_hash_key_str("key 4"), (void *) "val 4");
-    cxMapPut(map, cx_hash_key_str("key 5"), (void *) "val 5");
-    cxMapPut(map, cx_hash_key_str("key 6"), (void *) "val 6");
-    cxMapPut(map, cx_hash_key_str("key 7"), (void *) "val 7");
-
-    int result = cxMapRehash(map);
-    EXPECT_EQ(result, 0);
-    EXPECT_EQ(reinterpret_cast<struct cx_hash_map_s *>(map)->bucket_count, 17);
-    EXPECT_EQ(map->size, 7);
-
-    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 1")), "val 1"), 0);
-    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 2")), "val 2"), 0);
-    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 3")), "val 3"), 0);
-    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 4")), "val 4"), 0);
-    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 5")), "val 5"), 0);
-    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 6")), "val 6"), 0);
-    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 7")), "val 7"), 0);
-
-    cxMapDestroy(map);
-    EXPECT_TRUE(allocator.verify());
-}
-
-TEST(CxHashMap, Clear) {
-    CxTestingAllocator allocator;
-    auto map = cxHashMapCreate(&allocator, 0);
-    
-    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
-    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
-    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
-
-    EXPECT_EQ(map->size, 3);
-
-    cxMapClear(map);
-
-    EXPECT_EQ(map->size, 0);
-    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 1")), nullptr);
-    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 2")), nullptr);
-    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 3")), nullptr);
-
-    cxMapDestroy(map);
-    EXPECT_TRUE(allocator.verify());
-}
\ No newline at end of file
--- a/test/test_printf.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,248 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/printf.h"
-#include "cx/buffer.h"
-
-#include <gtest/gtest.h>
-#include "util_allocator.h"
-
-class PrintfFixture : public ::testing::Test {
-protected:
-    std::string buf;
-    CxTestingAllocator alloc;
-
-    void TearDown() override {
-        buf.clear();
-        ASSERT_TRUE(alloc.verify());
-    }
-
-    static size_t write_func(
-            void const *src,
-            size_t esize,
-            size_t ecount,
-            void *target
-    ) {
-        auto str = reinterpret_cast<char const *>(src);
-        auto buf = reinterpret_cast<std::string *>(target);
-        EXPECT_EQ(esize, 1);
-        EXPECT_EQ(strlen(str), ecount);
-        *buf = str;
-        return ecount;
-    }
-};
-
-
-TEST_F(PrintfFixture, BPrintf) {
-    CxBuffer buf;
-    cxBufferInit(&buf, nullptr, 64, &alloc, 0);
-
-    auto r = cx_bprintf(&buf, "This %s aged %u years in a %2XSK.", "Test", 10, 0xca);
-    EXPECT_EQ(r, 34);
-    EXPECT_EQ(buf.size, 34);
-    buf.space[r] = '\0';
-    EXPECT_STREQ(buf.space, "This Test aged 10 years in a CASK.");
-
-    cxBufferDestroy(&buf);
-}
-
-TEST_F(PrintfFixture, FPrintf) {
-    auto h = "Hello";
-    size_t r;
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "teststring");
-    EXPECT_EQ(r, 10);
-    EXPECT_EQ(buf, "teststring");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%10s]", h);
-    EXPECT_EQ(r, 12);
-    EXPECT_EQ(buf, "[     Hello]");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%-10s]", h);
-    EXPECT_EQ(r, 12);
-    EXPECT_EQ(buf, "[Hello     ]");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%*s]", 10, h);
-    EXPECT_EQ(r, 12);
-    EXPECT_EQ(buf, "[     Hello]");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%-10.*s]", 4, h);
-    EXPECT_EQ(r, 12);
-    EXPECT_EQ(buf, "[Hell      ]");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%-*.*s]", 10, 4, h);
-    EXPECT_EQ(r, 12);
-    EXPECT_EQ(buf, "[Hell      ]");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "%c", 'A');
-    EXPECT_EQ(r, 1);
-    EXPECT_EQ(buf, "A");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "%i %d %.6i %i %.0i %+i %i", 1, 2, 3, 0, 0, 4, -4);
-    EXPECT_EQ(r, 19);
-    EXPECT_EQ(buf, "1 2 000003 0  +4 -4");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "%x %x %X %#x", 5, 10, 10, 6);
-    EXPECT_EQ(r, 9);
-    EXPECT_EQ(buf, "5 a A 0x6");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "%o %#o %#o", 10, 10, 4);
-    EXPECT_EQ(r, 9);
-    EXPECT_EQ(buf, "12 012 04");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "%05.2f %.2f %5.2f", 1.5, 1.5, 1.5);
-    EXPECT_EQ(r, 16);
-    EXPECT_EQ(buf, "01.50 1.50  1.50");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "'%*c'", 5, 'x');
-    EXPECT_EQ(r, 7);
-    EXPECT_EQ(buf, "'    x'");
-
-    r = cx_fprintf(&buf, PrintfFixture::write_func, "'%*c'", -5, 'x');
-    EXPECT_EQ(r, 7);
-    EXPECT_EQ(buf, "'x    '");
-}
-
-TEST_F(PrintfFixture, BPrintfLargeString) {
-    CxBuffer buf;
-    cxBufferInit(&buf, nullptr, 64, &alloc, CX_BUFFER_AUTO_EXTEND);
-
-    auto aaa = std::string(512, 'a');
-    auto bbb = std::string(512, 'b');
-
-    auto r = cx_bprintf(&buf, "After %s comes %s.", aaa.data(), bbb.data());
-    EXPECT_EQ(r, 1038);
-    EXPECT_EQ(buf.size, 1038);
-    cxBufferPut(&buf, 0);
-    EXPECT_EQ(buf.space, std::string("After ") + aaa + " comes " + bbb + ".");
-
-    cxBufferDestroy(&buf);
-}
-
-TEST_F(PrintfFixture, BPrintfNoCap) {
-    CxBuffer buf;
-    char space[20];
-    memset(space, 'a', 20);
-    cxBufferInit(&buf, space, 16, &alloc, 0);
-
-    auto r = cx_bprintf(&buf, "Hello %s with more than %d chars.", "string", 16);
-    EXPECT_EQ(r, 16);
-    EXPECT_EQ(buf.size, 16);
-    EXPECT_EQ(0, memcmp(space, "Hello string witaaaa", 20));
-
-    cxBufferDestroy(&buf);
-}
-
-TEST_F(PrintfFixture, SPrintf) {
-    auto h = "Hello";
-
-    std::vector<char *> fl;
-    cxmutstr r;
-
-    r = cx_asprintf_a(&alloc, "teststring");
-    EXPECT_EQ(r.length, 10);
-    EXPECT_STREQ(r.ptr, "teststring");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "[%10s]", h);
-    EXPECT_EQ(r.length, 12);
-    EXPECT_STREQ(r.ptr, "[     Hello]");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "[%-10s]", h);
-    EXPECT_EQ(r.length, 12);
-    EXPECT_STREQ(r.ptr, "[Hello     ]");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "[%*s]", 10, h);
-    EXPECT_EQ(r.length, 12);
-    EXPECT_STREQ(r.ptr, "[     Hello]");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "[%-10.*s]", 4, h);
-    EXPECT_EQ(r.length, 12);
-    EXPECT_STREQ(r.ptr, "[Hell      ]");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "[%-*.*s]", 10, 4, h);
-    EXPECT_EQ(r.length, 12);
-    EXPECT_STREQ(r.ptr, "[Hell      ]");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "%c", 'A');
-    EXPECT_EQ(r.length, 1);
-    EXPECT_STREQ(r.ptr, "A");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "%i %d %.6i %i %.0i %+i %i", 1, 2, 3, 0, 0, 4, -4);
-    EXPECT_EQ(r.length, 19);
-    EXPECT_STREQ(r.ptr, "1 2 000003 0  +4 -4");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "%x %x %X %#x", 5, 10, 10, 6);
-    EXPECT_EQ(r.length, 9);
-    EXPECT_STREQ(r.ptr, "5 a A 0x6");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "%o %#o %#o", 10, 10, 4);
-    EXPECT_EQ(r.length, 9);
-    EXPECT_STREQ(r.ptr, "12 012 04");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "%05.2f %.2f %5.2f", 1.5, 1.5, 1.5);
-    EXPECT_EQ(r.length, 16);
-    EXPECT_STREQ(r.ptr, "01.50 1.50  1.50");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "'%*c'", 5, 'x');
-    EXPECT_EQ(r.length, 7);
-    EXPECT_STREQ(r.ptr, "'    x'");
-    fl.push_back(r.ptr);
-
-    r = cx_asprintf_a(&alloc, "'%*c'", -5, 'x');
-    EXPECT_EQ(r.length, 7);
-    EXPECT_STREQ(r.ptr, "'x    '");
-    fl.push_back(r.ptr);
-
-    for (auto c: fl) {
-        auto s = cx_mutstrn(c, 0);
-        cx_strfree_a(&alloc, &s);
-    }
-}
-
-TEST_F(PrintfFixture, SPrintfLargeString) {
-    auto aaa = std::string(512, 'a');
-    auto bbb = std::string(512, 'b');
-
-    auto r = cx_asprintf_a(&alloc, "After %s comes %s.", aaa.data(), bbb.data());
-    EXPECT_EQ(r.length, 1038);
-    EXPECT_EQ(r.ptr, std::string("After ") + aaa + " comes " + bbb + ".");
-    EXPECT_EQ(r.ptr[1038], '\0');
-
-    cx_strfree_a(&alloc, &r);
-}
\ No newline at end of file
--- a/test/test_string.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,865 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/string.h"
-#include "util_allocator.h"
-
-#include <gtest/gtest.h>
-
-#define EXPECT_ZERO_TERMINATED(str) EXPECT_EQ((str).ptr[(str).length], '\0')
-
-TEST(String, construct) {
-    cxstring s1 = cx_str("1234");
-    cxstring s2 = cx_strn("abcd", 2);
-    cxmutstr s3 = cx_mutstr((char *) "1234");
-    cxmutstr s4 = cx_mutstrn((char *) "abcd", 2);
-
-    EXPECT_EQ(s1.length, 4);
-    EXPECT_EQ(s2.length, 2);
-    EXPECT_EQ(s3.length, 4);
-    EXPECT_EQ(s4.length, 2);
-}
-
-TEST(String, strfree) {
-    CxTestingAllocator alloc;
-    auto test = (char *) cxMalloc(&alloc, 16);
-    cxmutstr str = cx_mutstrn(test, 16);
-    ASSERT_EQ(str.ptr, test);
-    EXPECT_EQ(str.length, 16);
-    cx_strfree_a(&alloc, &str);
-    EXPECT_EQ(str.ptr, nullptr);
-    EXPECT_EQ(str.length, 0);
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(String, strdup) {
-    cxstring str = CX_STR("test");
-    cxmutstr dup = cx_strdup(str);
-    ASSERT_EQ(dup.length, str.length);
-    EXPECT_STREQ(dup.ptr, str.ptr);
-    EXPECT_ZERO_TERMINATED(dup);
-    cx_strfree(&dup);
-
-    str.length = 2;
-    dup = cx_strdup(str);
-    ASSERT_EQ(dup.length, str.length);
-    EXPECT_STREQ(dup.ptr, "te");
-    EXPECT_ZERO_TERMINATED(dup);
-    cx_strfree(&dup);
-}
-
-TEST(String, strlen) {
-    cxstring s1 = CX_STR("1234");
-    cxstring s2 = CX_STR(".:.:.");
-    cxstring s3 = CX_STR("X");
-
-    size_t len0 = cx_strlen(0);
-    size_t len1 = cx_strlen(1, s1);
-    size_t len2 = cx_strlen(2, s1, s2);
-    size_t len3 = cx_strlen(3, s1, s2, s3);
-
-    EXPECT_EQ(len0, 0);
-    EXPECT_EQ(len1, 4);
-    EXPECT_EQ(len2, 9);
-    EXPECT_EQ(len3, 10);
-}
-
-TEST(String, strsubs) {
-    cxstring str = CX_STR("A test string");
-
-    cxstring sub = cx_strsubs(str, 0);
-    EXPECT_EQ(cx_strcmp(sub, str), 0);
-
-    sub = cx_strsubs(str, 2);
-    EXPECT_EQ(cx_strcmp(sub, cx_str("test string")), 0);
-
-    sub = cx_strsubs(str, 7);
-    EXPECT_EQ(cx_strcmp(sub, cx_str("string")), 0);
-
-    sub = cx_strsubs(str, 15);
-    EXPECT_EQ(cx_strcmp(sub, cx_str("")), 0);
-
-    sub = cx_strsubsl(str, 2, 4);
-    EXPECT_EQ(cx_strcmp(sub, cx_str("test")), 0);
-
-    sub = cx_strsubsl(str, 7, 3);
-    EXPECT_EQ(cx_strcmp(sub, cx_str("str")), 0);
-
-    sub = cx_strsubsl(str, 7, 20);
-    EXPECT_EQ(cx_strcmp(sub, cx_str("string")), 0);
-
-    // just for coverage, call the _m variant
-    auto m = cx_strsubs_m(cx_mutstrn(nullptr, 0), 0);
-    EXPECT_EQ(cx_strcmp(cx_strcast(m), cx_str("")), 0);
-}
-
-TEST(String, strchr) {
-    cxstring str = CX_STR("I will find you - and I will kill you");
-
-    cxstring notfound = cx_strchr(str, 'x');
-    EXPECT_EQ(notfound.length, 0);
-
-    cxstring result = cx_strchr(str, 'w');
-    EXPECT_EQ(result.length, 35);
-    EXPECT_STREQ(result.ptr, "will find you - and I will kill you");
-
-    // just for coverage, call the _m variant
-    auto m = cx_strchr_m(cx_mutstrn(nullptr, 0), 'a');
-    EXPECT_EQ(cx_strcmp(cx_strcast(m), cx_str("")), 0);
-}
-
-TEST(String, strrchr) {
-    cxstring str = CX_STR("I will find you - and I will kill you");
-
-    cxstring notfound = cx_strrchr(str, 'x');
-    EXPECT_EQ(notfound.length, 0);
-
-    cxstring result = cx_strrchr(str, 'w');
-    EXPECT_EQ(result.length, 13);
-    EXPECT_STREQ(result.ptr, "will kill you");
-
-    // just for coverage, call the _m variant
-    auto m = cx_strrchr_m(cx_mutstrn(nullptr, 0), 'a');
-    EXPECT_EQ(cx_strcmp(cx_strcast(m), cx_str("")), 0);
-}
-
-TEST(String, strstr) {
-    cxstring str = CX_STR("find the match in this string");
-    cxstring longstr = CX_STR(
-            "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
-            "mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx"
-            "yzabcdeababababnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij"
-            "klmnopqrstuvwxyzaababababababababrstuvwxyzabcdefghijklmnopqrstuv"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "wxyz1234567890");
-    cxstring longstrpattern = CX_STR(
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-    );
-    cxstring longstrresult = CX_STR(
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "abababababababababababababababababababababababababababababababab"
-            "wxyz1234567890"
-    );
-
-    cxstring notfound = cx_strstr(str, cx_str("no match"));
-    EXPECT_EQ(notfound.length, 0);
-
-    cxstring result = cx_strstr(str, cx_str("match"));
-    EXPECT_EQ(result.length, 20);
-    EXPECT_STREQ(result.ptr, "match in this string");
-
-    result = cx_strstr(str, cx_str(""));
-    EXPECT_EQ(result.length, str.length);
-    EXPECT_STREQ(result.ptr, str.ptr);
-
-    result = cx_strstr(longstr, longstrpattern);
-    EXPECT_EQ(result.length, longstrresult.length);
-    EXPECT_STREQ(result.ptr, longstrresult.ptr);
-
-    // just for coverage, call the _m variant
-    auto mstr = cx_strdup(longstr);
-    auto m = cx_strstr_m(mstr, longstrpattern);
-    EXPECT_EQ(m.length, longstrresult.length);
-    EXPECT_STREQ(m.ptr, longstrresult.ptr);
-    cx_strfree(&mstr);
-}
-
-TEST(String, strcmp) {
-    cxstring str = CX_STR("compare this");
-
-    EXPECT_EQ(cx_strcmp(cx_str(""), cx_str("")), 0);
-    EXPECT_GT(cx_strcmp(str, cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(str, cx_str("compare this")), 0);
-    EXPECT_NE(cx_strcmp(str, cx_str("Compare This")), 0);
-    EXPECT_LT(cx_strcmp(str, cx_str("compare tool")), 0);
-    EXPECT_GT(cx_strcmp(str, cx_str("compare shit")), 0);
-    EXPECT_LT(cx_strcmp(str, cx_str("compare this not")), 0);
-    EXPECT_GT(cx_strcmp(str, cx_str("compare")), 0);
-}
-
-TEST(String, strcasecmp) {
-    cxstring str = CX_STR("compare this");
-
-    EXPECT_EQ(cx_strcasecmp(cx_str(""), cx_str("")), 0);
-    EXPECT_GT(cx_strcasecmp(str, cx_str("")), 0);
-    EXPECT_EQ(cx_strcasecmp(str, cx_str("compare this")), 0);
-    EXPECT_EQ(cx_strcasecmp(str, cx_str("Compare This")), 0);
-    EXPECT_LT(cx_strcasecmp(str, cx_str("compare tool")), 0);
-    EXPECT_GT(cx_strcasecmp(str, cx_str("compare shit")), 0);
-    EXPECT_LT(cx_strcasecmp(str, cx_str("compare this not")), 0);
-    EXPECT_GT(cx_strcasecmp(str, cx_str("compare")), 0);
-}
-
-TEST(String, strcat) {
-    cxstring s1 = CX_STR("12");
-    cxstring s2 = CX_STR("34");
-    cxstring s3 = CX_STR("56");
-    cxstring sn = {nullptr, 0};
-
-    CxTestingAllocator alloc;
-
-    cxmutstr t1 = cx_strcat_a(&alloc, 2, s1, s2);
-    EXPECT_EQ(cx_strcmp(cx_strcast(t1), cx_str("1234")), 0);
-    EXPECT_ZERO_TERMINATED(t1);
-    cx_strfree_a(&alloc, &t1);
-
-    cxmutstr t2 = cx_strcat_a(&alloc, 3, s1, s2, s3);
-    EXPECT_EQ(cx_strcmp(cx_strcast(t2), cx_str("123456")), 0);
-    EXPECT_ZERO_TERMINATED(t2);
-    cx_strfree_a(&alloc, &t2);
-
-    cxmutstr t3 = cx_strcat_a(&alloc, 6, s1, sn, s2, sn, s3, sn);
-    EXPECT_EQ(cx_strcmp(cx_strcast(t3), cx_str("123456")), 0);
-    EXPECT_ZERO_TERMINATED(t3);
-    cx_strfree_a(&alloc, &t3);
-
-    cxmutstr t4 = cx_strcat_a(&alloc, 2, sn, sn);
-    EXPECT_EQ(cx_strcmp(cx_strcast(t4), cx_str("")), 0);
-    EXPECT_ZERO_TERMINATED(t4);
-    cx_strfree_a(&alloc, &t4);
-
-    EXPECT_TRUE(alloc.verify());
-
-    // use the macro
-    cxmutstr t5 = cx_strcat(3, s3, s1, s2);
-    EXPECT_EQ(cx_strcmp(cx_strcast(t5), cx_str("561234")), 0);
-    EXPECT_ZERO_TERMINATED(t5);
-    cx_strfree(&t5);
-}
-
-TEST(String, strsplit) {
-
-    cxstring test = cx_str("this,is,a,csv,string");
-    size_t capa = 8;
-    cxstring list[8];
-    size_t n;
-
-    // special case: empty string
-    n = cx_strsplit(test, cx_str(""), capa, list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-
-    // no delimiter occurrence
-    n = cx_strsplit(test, cx_str("z"), capa, list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-
-    // partially matching delimiter
-    n = cx_strsplit(test, cx_str("is,not"), capa, list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-
-    // matching single-char delimiter
-    n = cx_strsplit(test, cx_str(","), capa, list);
-    ASSERT_EQ(n, 5);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("a")), 0);
-    EXPECT_EQ(cx_strcmp(list[3], cx_str("csv")), 0);
-    EXPECT_EQ(cx_strcmp(list[4], cx_str("string")), 0);
-
-    // matching multi-char delimiter
-    n = cx_strsplit(test, cx_str("is"), capa, list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str(",")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str(",a,csv,string")), 0);
-
-    // bounded list using single-char delimiter
-    n = cx_strsplit(test, cx_str(","), 3, list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
-
-    // bounded list using multi-char delimiter
-    n = cx_strsplit(test, cx_str("is"), 2, list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
-
-    // start with delimiter
-    n = cx_strsplit(test, cx_str("this"), capa, list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
-
-    // end with delimiter
-    n = cx_strsplit(test, cx_str("string"), capa, list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("this,is,a,csv,")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
-
-
-    // end with delimiter exceed bound
-    n = cx_strsplit(cx_str("a,b,c,"), cx_str(","), 3, list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("a")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("b")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("c,")), 0);
-
-    // exact match
-    n = cx_strsplit(test, cx_str("this,is,a,csv,string"), capa, list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
-
-    // string to be split is only substring
-    n = cx_strsplit(test, cx_str("this,is,a,csv,string,with,extension"), capa, list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-
-    // subsequent encounter of delimiter (the string between is empty)
-    n = cx_strsplit(test, cx_str("is,"), capa, list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
-
-    // call the _m variant just for coverage
-    auto mtest = cx_strdup(test);
-    cxmutstr mlist[4];
-    n = cx_strsplit_m(mtest, cx_str("is,"), 4, mlist);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[0]), cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[1]), cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[2]), cx_str("a,csv,string")), 0);
-    cx_strfree(&mtest);
-}
-
-TEST(String, strsplit_a) {
-    CxTestingAllocator alloc;
-
-    cxstring test = cx_str("this,is,a,csv,string");
-    size_t capa = 8;
-    cxstring *list;
-    size_t n;
-
-    // special case: empty string
-    n = cx_strsplit_a(&alloc, test, cx_str(""), capa, &list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-    cxFree(&alloc, list);
-
-    // no delimiter occurrence
-    n = cx_strsplit_a(&alloc, test, cx_str("z"), capa, &list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-    cxFree(&alloc, list);
-
-    // partially matching delimiter
-    n = cx_strsplit_a(&alloc, test, cx_str("is,not"), capa, &list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-    cxFree(&alloc, list);
-
-    // matching single-char delimiter
-    n = cx_strsplit_a(&alloc, test, cx_str(","), capa, &list);
-    ASSERT_EQ(n, 5);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("a")), 0);
-    EXPECT_EQ(cx_strcmp(list[3], cx_str("csv")), 0);
-    EXPECT_EQ(cx_strcmp(list[4], cx_str("string")), 0);
-    cxFree(&alloc, list);
-
-    // matching multi-char delimiter
-    n = cx_strsplit_a(&alloc, test, cx_str("is"), capa, &list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str(",")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str(",a,csv,string")), 0);
-    cxFree(&alloc, list);
-
-    // bounded list using single-char delimiter
-    n = cx_strsplit_a(&alloc, test, cx_str(","), 3, &list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
-    cxFree(&alloc, list);
-
-    // bounded list using multi-char delimiter
-    n = cx_strsplit_a(&alloc, test, cx_str("is"), 2, &list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
-    cxFree(&alloc, list);
-
-    // start with delimiter
-    n = cx_strsplit_a(&alloc, test, cx_str("this"), capa, &list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
-    cxFree(&alloc, list);
-
-    // end with delimiter
-    n = cx_strsplit_a(&alloc, test, cx_str("string"), capa, &list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("this,is,a,csv,")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
-    cxFree(&alloc, list);
-
-    // end with delimiter exceed bound
-    n = cx_strsplit_a(&alloc, cx_str("a,b,c,"), cx_str(","), 3, &list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("a")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("b")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("c,")), 0);
-    cxFree(&alloc, list);
-
-    // exact match
-    n = cx_strsplit_a(&alloc, test, cx_str("this,is,a,csv,string"), capa, &list);
-    ASSERT_EQ(n, 2);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
-    cxFree(&alloc, list);
-
-    // string to be split is only substring
-    n = cx_strsplit_a(&alloc, test, cx_str("this,is,a,csv,string,with,extension"), capa, &list);
-    ASSERT_EQ(n, 1);
-    EXPECT_EQ(cx_strcmp(list[0], test), 0);
-    cxFree(&alloc, list);
-
-    // subsequent encounter of delimiter (the string between is empty)
-    n = cx_strsplit_a(&alloc, test, cx_str("is,"), capa, &list);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
-    cxFree(&alloc, list);
-
-    // call the _m variant just for coverage
-    auto mtest = cx_strdup(test);
-    cxmutstr *mlist;
-    n = cx_strsplit_ma(&alloc, mtest, cx_str("is,"), 4, &mlist);
-    ASSERT_EQ(n, 3);
-    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[0]), cx_str("th")), 0);
-    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[1]), cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[2]), cx_str("a,csv,string")), 0);
-    cxFree(&alloc, mlist);
-    cx_strfree(&mtest);
-
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(String, strtrim) {
-    cxstring t1 = cx_strtrim(cx_str("  ein test  \t "));
-    cxstring t2 = cx_strtrim(cx_str("abc"));
-    cxstring t3 = cx_strtrim(cx_str(" 123"));
-    cxstring t4 = cx_strtrim(cx_str("xyz "));
-    cxstring t5 = cx_strtrim(cx_str("   "));
-    cxstring empty = cx_strtrim(cx_str(""));
-
-    EXPECT_EQ(cx_strcmp(t1, cx_str("ein test")), 0);
-    EXPECT_EQ(cx_strcmp(t2, cx_str("abc")), 0);
-    EXPECT_EQ(cx_strcmp(t3, cx_str("123")), 0);
-    EXPECT_EQ(cx_strcmp(t4, cx_str("xyz")), 0);
-    EXPECT_EQ(cx_strcmp(t5, cx_str("")), 0);
-    EXPECT_EQ(cx_strcmp(empty, cx_str("")), 0);
-
-    // call the _m variant just for coverage
-    cxmutstr m1 = cx_strtrim_m(cx_mutstr((char *) "  ein test  \t "));
-    EXPECT_EQ(cx_strcmp(cx_strcast(m1), cx_str("ein test")), 0);
-}
-
-TEST(String, strprefix) {
-    cxstring str = CX_STR("test my prefix and my suffix");
-    cxstring empty = CX_STR("");
-    EXPECT_FALSE(cx_strprefix(empty, cx_str("pref")));
-    EXPECT_TRUE(cx_strprefix(str, empty));
-    EXPECT_TRUE(cx_strprefix(empty, empty));
-    EXPECT_TRUE(cx_strprefix(str, cx_str("test ")));
-    EXPECT_FALSE(cx_strprefix(str, cx_str("8-) fsck ")));
-}
-
-TEST(String, strsuffix) {
-    cxstring str = CX_STR("test my prefix and my suffix");
-    cxstring empty = CX_STR("");
-    EXPECT_FALSE(cx_strsuffix(empty, cx_str("suf")));
-    EXPECT_TRUE(cx_strsuffix(str, empty));
-    EXPECT_TRUE(cx_strsuffix(empty, empty));
-    EXPECT_TRUE(cx_strsuffix(str, cx_str("fix")));
-    EXPECT_FALSE(cx_strsuffix(str, cx_str("fox")));
-}
-
-TEST(String, strcaseprefix) {
-    cxstring str = CX_STR("test my prefix and my suffix");
-    cxstring empty = CX_STR("");
-    EXPECT_FALSE(cx_strcaseprefix(empty, cx_str("pREf")));
-    EXPECT_TRUE(cx_strcaseprefix(str, empty));
-    EXPECT_TRUE(cx_strcaseprefix(empty, empty));
-    EXPECT_TRUE(cx_strcaseprefix(str, cx_str("TEST ")));
-    EXPECT_FALSE(cx_strcaseprefix(str, cx_str("8-) fsck ")));
-}
-
-TEST(String, strcasesuffix) {
-    cxstring str = CX_STR("test my prefix and my suffix");
-    cxstring empty = CX_STR("");
-    EXPECT_FALSE(cx_strcasesuffix(empty, cx_str("sUf")));
-    EXPECT_TRUE(cx_strcasesuffix(str, empty));
-    EXPECT_TRUE(cx_strcasesuffix(empty, empty));
-    EXPECT_TRUE(cx_strcasesuffix(str, cx_str("FIX")));
-    EXPECT_FALSE(cx_strcasesuffix(str, cx_str("fox")));
-}
-
-TEST(String, strreplace) {
-    CxTestingAllocator alloc;
-    cxstring str = CX_STR("test ababab string aba");
-    cxstring longstr = CX_STR(
-            "xyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacd");
-    cxstring notrail = CX_STR("test abab");
-    cxstring empty = CX_STR("");
-    cxstring astr = CX_STR("aaaaaaaaaa");
-    cxstring csstr = CX_STR("test AB ab TEST xyz");
-
-    cxmutstr repl = cx_strreplace(str, cx_str("abab"), cx_str("muchlonger"));
-    auto expected = "test muchlongerab string aba";
-
-    cxmutstr repln = cx_strreplacen(str, cx_str("ab"), cx_str("c"), 2);
-    auto expectedn = "test ccab string aba";
-
-    cxmutstr longrepl = cx_strreplace(longstr, cx_str("a"), cx_str("z"));
-    auto longexpect = "xyzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzcd";
-
-    cxmutstr replnotrail = cx_strreplace(notrail, cx_str("ab"), cx_str("z"));
-    auto notrailexpect = "test zz";
-
-    cxmutstr repleq = cx_strreplace(str, str, cx_str("hello"));
-    auto eqexpect = "hello";
-
-    cxmutstr replempty1 = cx_strreplace(empty, cx_str("ab"), cx_str("c")); // expect: empty
-    cxmutstr replempty2 = cx_strreplace(str, cx_str("abab"), empty);
-    auto emptyexpect2 = "test ab string aba";
-
-    cxmutstr replpre = cx_strreplace(str, cx_str("test "), cx_str("TEST "));
-    auto preexpected = "TEST ababab string aba";
-
-    cxmutstr replan1 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 1);
-    auto an1expected = "xaaaaaaaaa";
-
-    cxmutstr replan4 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 4);
-    auto an4expected = "xxxxaaaaaa";
-
-    cxmutstr replan9 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 9);
-    auto an9expected = "xxxxxxxxxa";
-
-    cxmutstr replan10 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 10);
-    auto an10expected = "xxxxxxxxxx";
-
-    cxmutstr repl1_a = cx_strreplace_a(&alloc, csstr, cx_str("AB"), cx_str("*"));
-    auto expeced1_a = "test * ab TEST xyz";
-
-    cxmutstr repl2_a = cx_strreplace_a(&alloc, csstr, cx_str("test"), cx_str("TEST"));
-    auto expected2_a = "TEST AB ab TEST xyz";
-
-
-    EXPECT_NE(repl.ptr, str.ptr);
-    EXPECT_ZERO_TERMINATED(repl);
-    EXPECT_STREQ(repl.ptr, expected);
-    EXPECT_ZERO_TERMINATED(repln);
-    EXPECT_STREQ(repln.ptr, expectedn);
-    EXPECT_ZERO_TERMINATED(longrepl);
-    EXPECT_STREQ(longrepl.ptr, longexpect);
-    EXPECT_ZERO_TERMINATED(replnotrail);
-    EXPECT_STREQ(replnotrail.ptr, notrailexpect);
-    EXPECT_ZERO_TERMINATED(repleq);
-    EXPECT_STREQ(repleq.ptr, eqexpect);
-    EXPECT_ZERO_TERMINATED(replempty1);
-    EXPECT_STREQ(replempty1.ptr, "");
-    EXPECT_ZERO_TERMINATED(replempty2);
-    EXPECT_STREQ(replempty2.ptr, emptyexpect2);
-    EXPECT_ZERO_TERMINATED(replpre);
-    EXPECT_STREQ(replpre.ptr, preexpected);
-    EXPECT_ZERO_TERMINATED(replan1);
-    EXPECT_STREQ(replan1.ptr, an1expected);
-    EXPECT_ZERO_TERMINATED(replan4);
-    EXPECT_STREQ(replan4.ptr, an4expected);
-    EXPECT_ZERO_TERMINATED(replan9);
-    EXPECT_STREQ(replan9.ptr, an9expected);
-    EXPECT_ZERO_TERMINATED(replan10);
-    EXPECT_STREQ(replan10.ptr, an10expected);
-    EXPECT_ZERO_TERMINATED(repl1_a);
-    EXPECT_STREQ(repl1_a.ptr, expeced1_a);
-    EXPECT_ZERO_TERMINATED(repl2_a);
-    EXPECT_STREQ(repl2_a.ptr, expected2_a);
-
-    cx_strfree(&repl);
-    cx_strfree(&repln);
-    cx_strfree(&longrepl);
-    cx_strfree(&replnotrail);
-    cx_strfree(&repleq);
-    cx_strfree(&replempty1);
-    cx_strfree(&replempty2);
-    cx_strfree(&replpre);
-    cx_strfree(&replan1);
-    cx_strfree(&replan4);
-    cx_strfree(&replan9);
-    cx_strfree(&replan10);
-
-    cx_strfree_a(&alloc, &repl1_a);
-    cx_strfree_a(&alloc, &repl2_a);
-    EXPECT_TRUE(alloc.verify());
-}
-
-TEST(String, strupper) {
-    cxmutstr str = cx_strdup(cx_str("thIs 1s @ Te$t"));
-    cx_strupper(str);
-    EXPECT_STREQ(str.ptr, "THIS 1S @ TE$T");
-    cx_strfree(&str);
-}
-
-TEST(String, strlower) {
-    cxmutstr str = cx_strdup(cx_str("thIs 1s @ Te$t"));
-    cx_strlower(str);
-    EXPECT_STREQ(str.ptr, "this 1s @ te$t");
-    cx_strfree(&str);
-}
-
-TEST(String, strtok) {
-    cxstring str = cx_str("a,comma,separated,string");
-    cxstring delim = cx_str(",");
-    CxStrtokCtx ctx = cx_strtok(str, delim, 3);
-    EXPECT_EQ(ctx.str.ptr, str.ptr);
-    EXPECT_EQ(ctx.str.length, str.length);
-    EXPECT_EQ(ctx.delim.ptr, delim.ptr);
-    EXPECT_EQ(ctx.delim.length, delim.length);
-    EXPECT_EQ(ctx.limit, 3);
-    EXPECT_EQ(ctx.found, 0);
-    EXPECT_EQ(ctx.pos, 0);
-    EXPECT_EQ(ctx.next_pos, 0);
-    EXPECT_EQ(ctx.delim_more, nullptr);
-    EXPECT_EQ(ctx.delim_more_count, 0);
-}
-
-TEST(String, strtok_m) {
-    cxmutstr str = cx_strdup(cx_str("a,comma,separated,string"));
-    cxstring delim = cx_str(",");
-    CxStrtokCtx ctx = cx_strtok_m(str, delim, 3);
-    EXPECT_EQ(ctx.str.ptr, str.ptr);
-    EXPECT_EQ(ctx.str.length, str.length);
-    EXPECT_EQ(ctx.delim.ptr, delim.ptr);
-    EXPECT_EQ(ctx.delim.length, delim.length);
-    EXPECT_EQ(ctx.limit, 3);
-    EXPECT_EQ(ctx.found, 0);
-    EXPECT_EQ(ctx.pos, 0);
-    EXPECT_EQ(ctx.next_pos, 0);
-    EXPECT_EQ(ctx.delim_more, nullptr);
-    EXPECT_EQ(ctx.delim_more_count, 0);
-    cx_strfree(&str);
-}
-
-TEST(String, strtok_delim) {
-    cxstring str = cx_str("an,arbitrarily|separated;string");
-    cxstring delim = cx_str(",");
-    cxstring delim_more[2] = {CX_STR("|"), CX_STR(";")};
-    CxStrtokCtx ctx = cx_strtok(str, delim, 3);
-    cx_strtok_delim(&ctx, delim_more, 2);
-    EXPECT_EQ(ctx.str.ptr, str.ptr);
-    EXPECT_EQ(ctx.str.length, str.length);
-    EXPECT_EQ(ctx.delim.ptr, delim.ptr);
-    EXPECT_EQ(ctx.delim.length, delim.length);
-    EXPECT_EQ(ctx.limit, 3);
-    EXPECT_EQ(ctx.found, 0);
-    EXPECT_EQ(ctx.pos, 0);
-    EXPECT_EQ(ctx.next_pos, 0);
-    EXPECT_EQ(ctx.delim_more, delim_more);
-    EXPECT_EQ(ctx.delim_more_count, 2);
-}
-
-TEST(String, strtok_next_easy) {
-    cxstring str = cx_str("a,comma,separated,string");
-    cxstring delim = cx_str(",");
-    CxStrtokCtx ctx = cx_strtok(str, delim, 3);
-    bool ret;
-    cxstring tok;
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("a")), 0);
-    EXPECT_EQ(ctx.pos, 0);
-    EXPECT_EQ(ctx.next_pos, 2);
-    EXPECT_EQ(ctx.delim_pos, 1);
-    EXPECT_EQ(ctx.found, 1);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("comma")), 0);
-    EXPECT_EQ(ctx.pos, 2);
-    EXPECT_EQ(ctx.next_pos, 8);
-    EXPECT_EQ(ctx.delim_pos, 7);
-    EXPECT_EQ(ctx.found, 2);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("separated")), 0);
-    EXPECT_EQ(ctx.pos, 8);
-    EXPECT_EQ(ctx.next_pos, 18);
-    EXPECT_EQ(ctx.delim_pos, 17);
-    EXPECT_EQ(ctx.found, 3);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_FALSE(ret);
-    EXPECT_EQ(ctx.pos, 8);
-    EXPECT_EQ(ctx.next_pos, 18);
-    EXPECT_EQ(ctx.delim_pos, 17);
-    EXPECT_EQ(ctx.found, 3);
-}
-
-TEST(String, strtok_next_unlimited) {
-    cxstring str = cx_str("some;-;otherwise;-;separated;-;string;-;");
-    cxstring delim = cx_str(";-;");
-    CxStrtokCtx ctx = cx_strtok(str, delim, SIZE_MAX);
-    bool ret;
-    cxstring tok;
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("some")), 0);
-    EXPECT_EQ(ctx.pos, 0);
-    EXPECT_EQ(ctx.next_pos, 7);
-    EXPECT_EQ(ctx.delim_pos, 4);
-    EXPECT_EQ(ctx.found, 1);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("otherwise")), 0);
-    EXPECT_EQ(ctx.pos, 7);
-    EXPECT_EQ(ctx.next_pos, 19);
-    EXPECT_EQ(ctx.delim_pos, 16);
-    EXPECT_EQ(ctx.found, 2);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("separated")), 0);
-    EXPECT_EQ(ctx.pos, 19);
-    EXPECT_EQ(ctx.next_pos, 31);
-    EXPECT_EQ(ctx.delim_pos, 28);
-    EXPECT_EQ(ctx.found, 3);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("string")), 0);
-    EXPECT_EQ(ctx.pos, 31);
-    EXPECT_EQ(ctx.next_pos, 40);
-    EXPECT_EQ(ctx.delim_pos, 37);
-    EXPECT_EQ(ctx.found, 4);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(tok, cx_str("")), 0);
-    EXPECT_EQ(ctx.pos, 40);
-    EXPECT_EQ(ctx.next_pos, 40);
-    EXPECT_EQ(ctx.delim_pos, 40);
-    EXPECT_EQ(ctx.found, 5);
-
-    ret = cx_strtok_next(&ctx, &tok);
-    ASSERT_FALSE(ret);
-    EXPECT_EQ(ctx.pos, 40);
-    EXPECT_EQ(ctx.delim_pos, 40);
-    EXPECT_EQ(ctx.found, 5);
-}
-
-TEST(String, strtok_next_advanced) {
-    cxmutstr str = cx_strdup(cx_str("an,arbitrarily;||separated;string"));
-    cxstring delim = cx_str(",");
-    cxstring delim_more[2] = {CX_STR("||"), CX_STR(";")};
-    CxStrtokCtx ctx = cx_strtok_m(str, delim, 10);
-    cx_strtok_delim(&ctx, delim_more, 2);
-    bool ret;
-    cxmutstr tok;
-
-    ret = cx_strtok_next_m(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("an")), 0);
-    EXPECT_EQ(ctx.pos, 0);
-    EXPECT_EQ(ctx.next_pos, 3);
-    EXPECT_EQ(ctx.delim_pos, 2);
-    EXPECT_EQ(ctx.found, 1);
-    cx_strupper(tok);
-
-    ret = cx_strtok_next_m(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("arbitrarily")), 0);
-    EXPECT_EQ(ctx.pos, 3);
-    EXPECT_EQ(ctx.next_pos, 15);
-    EXPECT_EQ(ctx.delim_pos, 14);
-    EXPECT_EQ(ctx.found, 2);
-    cx_strupper(tok);
-
-    ret = cx_strtok_next_m(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("")), 0);
-    EXPECT_EQ(ctx.pos, 15);
-    EXPECT_EQ(ctx.next_pos, 17);
-    EXPECT_EQ(ctx.delim_pos, 15);
-    EXPECT_EQ(ctx.found, 3);
-    cx_strupper(tok);
-
-    ret = cx_strtok_next_m(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("separated")), 0);
-    EXPECT_EQ(ctx.pos, 17);
-    EXPECT_EQ(ctx.next_pos, 27);
-    EXPECT_EQ(ctx.delim_pos, 26);
-    EXPECT_EQ(ctx.found, 4);
-    cx_strupper(tok);
-
-    ret = cx_strtok_next_m(&ctx, &tok);
-    ASSERT_TRUE(ret);
-    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("string")), 0);
-    EXPECT_EQ(ctx.pos, 27);
-    EXPECT_EQ(ctx.next_pos, 33);
-    EXPECT_EQ(ctx.delim_pos, 33);
-    EXPECT_EQ(ctx.found, 5);
-    cx_strupper(tok);
-
-    ret = cx_strtok_next_m(&ctx, &tok);
-    ASSERT_FALSE(ret);
-    EXPECT_EQ(ctx.pos, 27);
-    EXPECT_EQ(ctx.next_pos, 33);
-    EXPECT_EQ(ctx.delim_pos, 33);
-    EXPECT_EQ(ctx.found, 5);
-
-    EXPECT_EQ(cx_strcmp(cx_strcast(str), cx_str("AN,ARBITRARILY;||SEPARATED;STRING")), 0);
-
-    cx_strfree(&str);
-}
--- a/test/test_tree.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/tree.h"
-#include <gtest/gtest.h>
-
-struct TestNode {
-    TestNode *parent = nullptr;
-    TestNode *prev = nullptr;
-    TestNode *next = nullptr;
-
-    TestNode *children_begin = nullptr;
-    TestNode *children_end = nullptr;
-};
-
-TEST(Tree, cx_tree_add_sibling) {
-    // prepare test tree
-    TestNode root, a;
-    root.children_begin = &a;
-    root.children_end = &a;
-    a.parent = &root;
-
-    // new test nodes
-    TestNode b, c;
-
-    // test
-    cx_tree_add_sibling(&a, offsetof(TestNode, prev), offsetof(TestNode, next), offsetof(TestNode, parent), &b);
-    EXPECT_EQ(b.parent, &root);
-    EXPECT_EQ(b.prev, &a);
-    EXPECT_EQ(b.next, nullptr);
-    EXPECT_EQ(a.next, &b);
-
-    cx_tree_add_sibling(&a, -1, offsetof(TestNode, next), -1, &c);
-    EXPECT_EQ(c.parent, nullptr);
-    EXPECT_EQ(c.prev, nullptr);
-    EXPECT_EQ(c.next, nullptr);
-    EXPECT_EQ(b.next, &c);
-}
-
-TEST(Tree, cx_tree_add_child) {
-    TestNode root, a, b, c, a1;
-
-    cx_tree_add_child(
-            (void **) &root.children_begin,
-            (void **) &root.children_end,
-            offsetof(TestNode, prev),
-            offsetof(TestNode, next),
-            &a,
-            offsetof(TestNode, parent),
-            &root);
-    EXPECT_EQ(root.children_begin, &a);
-    EXPECT_EQ(root.children_end, &a);
-    EXPECT_EQ(a.parent, &root);
-    EXPECT_EQ(a.prev, nullptr);
-    EXPECT_EQ(a.next, nullptr);
-
-    cx_tree_add_child(
-            (void **) &root.children_begin,
-            (void **) &root.children_end,
-            offsetof(TestNode, prev),
-            offsetof(TestNode, next),
-            &b,
-            offsetof(TestNode, parent),
-            &root);
-    EXPECT_EQ(root.children_begin, &a);
-    EXPECT_EQ(root.children_begin->next, &b);
-    EXPECT_EQ(root.children_end, &b);
-    EXPECT_EQ(b.parent, &root);
-    EXPECT_EQ(b.prev, &a);
-
-    cx_tree_add_child(
-            (void **) &root.children_begin,
-            nullptr,
-            -1,
-            offsetof(TestNode, next),
-            &c,
-            -1,
-            &root);
-    EXPECT_EQ(root.children_end, &b); // children_end unchanged
-    EXPECT_EQ(b.next, &c);
-    EXPECT_EQ(c.prev, nullptr);
-    EXPECT_EQ(c.next, nullptr);
-    EXPECT_EQ(c.parent, nullptr);
-
-    cx_tree_add_child(
-            (void **) &a.children_begin,
-            (void **) &a.children_end,
-            offsetof(TestNode, prev),
-            offsetof(TestNode, next),
-            &a1,
-            offsetof(TestNode, parent),
-            &a);
-    EXPECT_EQ(a.children_begin, &a1);
-    EXPECT_EQ(a1.parent, &a);
-    EXPECT_EQ(root.children_begin, &a);
-    EXPECT_EQ(root.children_begin->children_begin, &a1);
-}
--- a/test/test_utils.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/utils.h"
-
-#include <gtest/gtest.h>
-
-TEST(Utils, ForN) {
-    unsigned j;
-    j = 0;
-    cx_for_n(i, 50) {
-        EXPECT_EQ(i, j);
-        j++;
-    }
-}
-
-TEST(Utils, szmul) {
-    size_t r;
-    int e;
-    e = cx_szmul(5, 7, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(35, r);
-
-    size_t s = SIZE_MAX & ~3;
-
-    e = cx_szmul(s / 4, 2, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s / 2, r);
-    e = cx_szmul(2, s / 4, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s / 2, r);
-
-    e = cx_szmul(s / 4, 4, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s, r);
-
-    e = cx_szmul(4, s / 4, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s, r);
-
-    e = cx_szmul(s / 4, 5, &r);
-    EXPECT_NE(0, e);
-
-    e = cx_szmul(5, s / 4, &r);
-    EXPECT_NE(0, e);
-
-    e = cx_szmul(SIZE_MAX - 4, 0, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul(0, SIZE_MAX - 1, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul(SIZE_MAX, 0, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul(0, SIZE_MAX, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul(0, 0, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-}
-
-#ifdef CX_SZMUL_BUILTIN
-
-// also test the custom implementation
-struct Utils_szmul_impl : ::testing::Test {
-#undef CX_SZMUL_BUILTIN
-
-#include "../src/utils.c"
-
-#define CX_SZMUL_BUILTIN
-};
-
-TEST_F(Utils_szmul_impl, Test) {
-    size_t r;
-    int e;
-    e = cx_szmul_impl(5, 7, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(35, r);
-
-    size_t s = SIZE_MAX & ~3;
-
-    e = cx_szmul_impl(s / 4, 2, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s / 2, r);
-    e = cx_szmul_impl(2, s / 4, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s / 2, r);
-
-    e = cx_szmul_impl(s / 4, 4, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s, r);
-
-    e = cx_szmul_impl(4, s / 4, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(s, r);
-
-    e = cx_szmul_impl(s / 4, 5, &r);
-    EXPECT_NE(0, e);
-
-    e = cx_szmul_impl(5, s / 4, &r);
-    EXPECT_NE(0, e);
-
-    e = cx_szmul_impl(SIZE_MAX - 4, 0, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul_impl(0, SIZE_MAX - 1, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul_impl(SIZE_MAX, 0, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul_impl(0, SIZE_MAX, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-
-    e = cx_szmul_impl(0, 0, &r);
-    EXPECT_EQ(0, e);
-    EXPECT_EQ(0, r);
-}
-
-#endif // CX_SZMUL_BUILTIN
--- a/test/util_allocator.cpp	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "util_allocator.h"
-
-void *cx_malloc_testing(void *d, size_t n) {
-    auto data = reinterpret_cast<CxTestingAllocator *>(d);
-    void *ptr = malloc(n);
-    data->alloc_total++;
-    if (ptr == nullptr) {
-        data->alloc_failed++;
-    } else {
-        data->tracked.insert(ptr);
-    }
-    return ptr;
-}
-
-void *cx_realloc_testing(void *d, void *mem, size_t n) {
-    auto data = reinterpret_cast<CxTestingAllocator *>(d);
-    void *ptr = realloc(mem, n);
-    if (ptr == mem) {
-        return ptr;
-    } else {
-        data->alloc_total++;
-        if (ptr == nullptr) {
-            data->alloc_failed++;
-        } else {
-            data->free_total++;
-            if (data->tracked.erase(mem) == 0) {
-                data->free_failed++;
-            }
-            data->tracked.insert(ptr);
-        }
-        return ptr;
-    }
-}
-
-void *cx_calloc_testing(void *d, size_t nelem, size_t n) {
-    auto data = reinterpret_cast<CxTestingAllocator *>(d);
-    void *ptr = calloc(nelem, n);
-    data->alloc_total++;
-    if (ptr == nullptr) {
-        data->alloc_failed++;
-    } else {
-        data->tracked.insert(ptr);
-    }
-    return ptr;
-}
-
-void cx_free_testing(void *d, void *mem) {
-    auto data = reinterpret_cast<CxTestingAllocator *>(d);
-    data->free_total++;
-    if (data->tracked.erase(mem) == 0) {
-        data->free_failed++;
-        // do not even attempt to free mem, because it is likely to segfault
-    } else {
-        free(mem);
-    }
-}
-
-cx_allocator_class cx_testing_allocator_class = {
-        cx_malloc_testing,
-        cx_realloc_testing,
-        cx_calloc_testing,
-        cx_free_testing
-};
-
-CxTestingAllocator::CxTestingAllocator() : CxAllocator() {
-    cl = &cx_testing_allocator_class;
-    data = this;
-}
-
-bool CxTestingAllocator::used() const {
-    return alloc_total > 0;
-}
-
-bool CxTestingAllocator::verify() const {
-    return tracked.empty() && alloc_failed == 0 && free_failed == 0 && alloc_total == free_total;
-}
-
-// SELF-TEST
-
-#include <gtest/gtest.h>
-
-TEST(TestingAllocator, ExpectFree) {
-    CxTestingAllocator allocator;
-
-    ASSERT_TRUE(allocator.verify());
-    EXPECT_FALSE(allocator.used());
-    auto ptr = cxMalloc(&allocator, 16);
-    EXPECT_TRUE(allocator.used());
-    ASSERT_NE(ptr, nullptr);
-    EXPECT_FALSE(allocator.verify());
-
-    cxFree(&allocator, ptr);
-    EXPECT_TRUE(allocator.verify());
-}
-
-TEST(TestingAllocator, DetectDoubleFree) {
-    CxTestingAllocator allocator;
-
-    ASSERT_TRUE(allocator.verify());
-    auto ptr = cxMalloc(&allocator, 16);
-    ASSERT_NE(ptr, nullptr);
-
-    cxFree(&allocator, ptr);
-    EXPECT_TRUE(allocator.verify());
-    ASSERT_NO_FATAL_FAILURE(cxFree(&allocator, ptr));
-    EXPECT_FALSE(allocator.verify());
-}
-
-TEST(TestingAllocator, FreeUntracked) {
-    CxTestingAllocator allocator;
-
-    auto ptr = malloc(16);
-    ASSERT_TRUE(allocator.verify());
-    ASSERT_NO_FATAL_FAILURE(cxFree(&allocator, ptr));
-    EXPECT_FALSE(allocator.verify());
-    ASSERT_NO_FATAL_FAILURE(free(ptr));
-}
-
-TEST(TestingAllocator, FullLifecycleWithRealloc) {
-    CxTestingAllocator allocator;
-    ASSERT_TRUE(allocator.verify());
-    auto ptr = cxMalloc(&allocator, 16);
-    ASSERT_NE(ptr, nullptr);
-    EXPECT_EQ(allocator.tracked.size(), 1);
-    ptr = cxRealloc(&allocator, ptr, 256);
-    ASSERT_NE(ptr, nullptr);
-    EXPECT_EQ(allocator.tracked.size(), 1);
-    cxFree(&allocator, ptr);
-    EXPECT_TRUE(allocator.verify());
-}
-
-TEST(TestingAllocator, CallocInitializes) {
-    CxTestingAllocator allocator;
-    const char zeros[16] = {0};
-    auto ptr = cxCalloc(&allocator, 16, 1);
-    EXPECT_EQ(memcmp(ptr, zeros, 16), 0);
-    cxFree(&allocator, ptr);
-    EXPECT_TRUE(allocator.verify());
-}
--- a/test/util_allocator.h	Tue Feb 07 21:53:06 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef UCX_UTIL_ALLOCATOR_H
-#define UCX_UTIL_ALLOCATOR_H
-
-#include "cx/allocator.h"
-
-#include <set>
-
-struct CxTestingAllocator : public CxAllocator {
-    /**
-     * Total number of all allocations (malloc, calloc, realloc).
-     * A realloc() does only count when the memory has to be moved.
-     */
-    unsigned alloc_total = 0;
-    /**
-     * Number of failed allocations (malloc, calloc, realloc).
-     */
-    unsigned alloc_failed = 0;
-    /**
-     * Total number of freed pointers.
-     * A reallocation also counts as a free when the memory has to be moved.
-     */
-    unsigned free_total = 0;
-    /**
-     * Number of failed free invocations.
-     * A free() is considered failed, if it has not been performed on tracked memory.
-     */
-    unsigned free_failed = 0;
-    /**
-     * The set of tracked memory blocks.
-     */
-    std::set<void *> tracked;
-
-    /**
-     * Constructs a new testing allocator.
-     */
-    CxTestingAllocator();
-
-    /**
-     * Verifies that this allocator has been used.
-     *
-     * @return true if any allocation was attempted using this allocator
-     */
-    [[nodiscard]] bool used() const;
-
-    /**
-     * Verifies that all allocated memory blocks are freed and no free occurred twice.
-     *
-     * @return true iff all tracked allocations / deallocations were valid
-     */
-    [[nodiscard]] bool verify() const;
-};
-
-#endif // UCX_UTIL_ALLOCATOR_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/.clang-tidy	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,2 @@
+# Disable static initialization warning for test code
+Checks: '-cert-err58-cpp'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/CMakeLists.txt	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,32 @@
+# Load Google Test Framework
+set(CMAKE_CXX_STANDARD 17)
+
+include(FetchContent)
+FetchContent_Declare(
+        googletest
+        GIT_REPOSITORY https://github.com/google/googletest.git
+        GIT_TAG e2239ee6043f73722e7aa812a459f54a28552929 # release 1.11.0
+)
+# For Windows: Prevent overriding the parent project's compiler/linker settings
+set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+FetchContent_MakeAvailable(googletest)
+include(GoogleTest)
+message(STATUS "Google Test made available")
+
+add_executable(ucxtest
+        test_utils.cpp
+        test_allocator.cpp
+        test_compare.cpp
+        test_string.cpp
+        test_buffer.cpp
+        test_list.cpp
+        test_tree.cpp
+        test_hash_key.cpp
+        test_map.cpp
+        test_basic_mempool.cpp
+        test_printf.cpp
+        selftest.cpp
+        util_allocator.cpp
+        )
+target_link_libraries(ucxtest PRIVATE ucx_static gtest_main)
+gtest_discover_tests(ucxtest)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/selftest.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,39 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <gtest/gtest.h>
+#include <cx/common.h>
+
+TEST(SelfTest, BasicAssertion) {
+    EXPECT_EQ(7 * 6, 42);
+}
+
+TEST(SelfTest, UcxVersion) {
+    EXPECT_GE(UCX_VERSION_MAJOR, 3);
+    EXPECT_GE(UCX_VERSION, 3 << 16);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_allocator.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,96 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/allocator.h"
+#include <gtest/gtest.h>
+
+TEST(Allocator, DefaultAllocator) {
+    cx_allocator_class *clazz = cxDefaultAllocator->cl;
+    ASSERT_NE(clazz, nullptr);
+}
+
+TEST(Allocator, DefaultMalloc) {
+    void *test = cxMalloc(cxDefaultAllocator, 16);
+    ASSERT_NE(test, nullptr);
+    free(test);
+}
+
+TEST(Allocator, DefaultRealloc) {
+    void *test = calloc(8, 1);
+    memcpy(test, "Test", 5);
+    test = cxRealloc(cxDefaultAllocator, test, 16);
+    ASSERT_NE(test, nullptr);
+    EXPECT_STREQ(reinterpret_cast<char *>(test), "Test");
+    free(test);
+}
+
+TEST(Allocator, Reallocate) {
+    void *test = calloc(8, 1);
+    memcpy(test, "Test", 5);
+    int ret = cxReallocate(cxDefaultAllocator, &test, 16);
+    EXPECT_EQ(ret, 0);
+    ASSERT_NE(test, nullptr);
+    EXPECT_STREQ(reinterpret_cast<char *>(test), "Test");
+    free(test);
+}
+
+TEST(Allocator, DefaultCalloc) {
+    char *test = reinterpret_cast<char *>(cxCalloc(cxDefaultAllocator, 8, 2));
+    ASSERT_NE(test, nullptr);
+    for (int i = 0; i < 16; i++) ASSERT_EQ(test[i], 0);
+    free(test);
+}
+
+TEST(Allocator, DefaultFree) {
+    void *test = malloc(16);
+    EXPECT_NO_FATAL_FAILURE(
+            cxFree(cxDefaultAllocator, test);
+    );
+}
+
+TEST(Allocator, FailingReallocate) {
+    // Mock an allocator that always returns nullptr on realloc
+    cx_allocator_class mock_cl;
+    mock_cl.realloc = [](
+            [[maybe_unused]]void *p,
+            [[maybe_unused]]void *d,
+            [[maybe_unused]]size_t n
+    ) -> void * { return nullptr; };
+    cx_allocator_s mock{&mock_cl, nullptr};
+
+    void *test = calloc(8, 1);
+    memcpy(test, "Test", 5);
+    void *original = test;
+    int ret = cxReallocate(&mock, &test, 16);
+    // non-zero return code because of the failure
+    EXPECT_NE(ret, 0);
+    // the test pointer was not changed and still points to the same memory
+    EXPECT_EQ(test, original);
+    EXPECT_STREQ(reinterpret_cast<char *>(test), "Test");
+    free(test);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_basic_mempool.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,154 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/basic_mempool.h"
+#include "util_allocator.h"
+#include <gtest/gtest.h>
+
+class CxBasicMempool : public ::testing::Test {
+protected:
+    CxMempool *pool = nullptr;
+
+    void TearDown() override {
+        if (pool != nullptr) {
+            cxMempoolDestroy(pool);
+        }
+    }
+};
+
+TEST_F(CxBasicMempool, Create) {
+    pool = cxBasicMempoolCreate(16);
+    ASSERT_NE(pool->allocator, nullptr);
+    ASSERT_NE(pool->cl, nullptr);
+    EXPECT_NE(pool->cl->destroy, nullptr);
+    ASSERT_NE(pool->allocator->cl, nullptr);
+    EXPECT_EQ(pool->allocator->data, pool);
+    EXPECT_NE(pool->allocator->cl->malloc, nullptr);
+    EXPECT_NE(pool->allocator->cl->calloc, nullptr);
+    EXPECT_NE(pool->allocator->cl->realloc, nullptr);
+    EXPECT_NE(pool->allocator->cl->free, nullptr);
+
+    auto basic_pool = reinterpret_cast<cx_basic_mempool_s *>(pool);
+    EXPECT_EQ(basic_pool->size, 16);
+    EXPECT_EQ(basic_pool->ndata, 0);
+    EXPECT_NE(basic_pool->data, nullptr);
+}
+
+TEST_F(CxBasicMempool, malloc) {
+    pool = cxBasicMempoolCreate(4);
+    auto basic_pool = reinterpret_cast<cx_basic_mempool_s *>(pool);
+    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
+    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
+    EXPECT_EQ(basic_pool->ndata, 2);
+    EXPECT_EQ(basic_pool->size, 4);
+    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
+    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
+    EXPECT_EQ(basic_pool->ndata, 4);
+    EXPECT_EQ(basic_pool->size, 4);
+    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
+    EXPECT_NE(cxMalloc(pool->allocator, sizeof(int)), nullptr);
+    EXPECT_EQ(basic_pool->ndata, 6);
+    EXPECT_GE(basic_pool->size, 6);
+}
+
+TEST_F(CxBasicMempool, calloc) {
+    pool = cxBasicMempoolCreate(4);
+
+    auto test = (int *) cxCalloc(pool->allocator, 2, sizeof(int));
+    ASSERT_NE(test, nullptr);
+    EXPECT_EQ(test[0], 0);
+    EXPECT_EQ(test[1], 0);
+}
+
+static unsigned test_destructor_called = 0;
+
+static void test_destructor([[maybe_unused]] void *mem) {
+    test_destructor_called++;
+}
+
+TEST_F(CxBasicMempool, destructor) {
+    pool = cxBasicMempoolCreate(4);
+    auto data = cxMalloc(pool->allocator, sizeof(int));
+    *((int *) data) = 13;
+    cxMempoolSetDestructor(pool, data, test_destructor);
+    EXPECT_EQ(*((int *) data), 13);
+    test_destructor_called = 0;
+    cxFree(pool->allocator, data);
+    EXPECT_EQ(test_destructor_called, 1);
+    data = cxMalloc(pool->allocator, sizeof(int));
+    cxMempoolSetDestructor(pool, data, test_destructor);
+    cxMempoolDestroy(pool);
+    pool = nullptr;
+    EXPECT_EQ(test_destructor_called, 2);
+}
+
+TEST_F(CxBasicMempool, realloc) {
+    pool = cxBasicMempoolCreate(4);
+    auto data = cxMalloc(pool->allocator, sizeof(int));
+    *((int *) data) = 13;
+    cxMempoolSetDestructor(pool, data, test_destructor);
+
+    void *rdata = data;
+    unsigned n = 1;
+    while (rdata == data) {
+        n <<= 1;
+        ASSERT_LT(n, 65536); // eventually the memory should be moved elsewhere
+        rdata = cxRealloc(pool->allocator, data, n * sizeof(intptr_t));
+    }
+
+    EXPECT_EQ(*((int *) rdata), 13);
+    // test if destructor is still intact
+    test_destructor_called = 0;
+    cxFree(pool->allocator, rdata);
+    EXPECT_EQ(test_destructor_called, 1);
+}
+
+
+TEST_F(CxBasicMempool, free) {
+    pool = cxBasicMempoolCreate(4);
+    auto basic_pool = reinterpret_cast<cx_basic_mempool_s *>(pool);
+
+    void *mem1;
+    void *mem2;
+
+    mem1 = cxMalloc(pool->allocator, 16);
+    cxFree(pool->allocator, mem1);
+    EXPECT_EQ(basic_pool->ndata, 0);
+
+    cxMalloc(pool->allocator, 16);
+    cxMalloc(pool->allocator, 16);
+    mem1 = cxMalloc(pool->allocator, 16);
+    cxMalloc(pool->allocator, 16);
+    mem2 = cxMalloc(pool->allocator, 16);
+
+    EXPECT_EQ(basic_pool->ndata, 5);
+    cxFree(pool->allocator, mem1);
+    EXPECT_EQ(basic_pool->ndata, 4);
+    cxFree(pool->allocator, mem2);
+    EXPECT_EQ(basic_pool->ndata, 3);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_buffer.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,815 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/buffer.h"
+
+#include <gtest/gtest.h>
+#include "util_allocator.h"
+
+class BufferFixture : public ::testing::Test {
+protected:
+    void SetUp() override {
+        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
+        buf.size = 6;
+        buf.pos = 3;
+    }
+
+    void TearDown() override {
+        cxBufferDestroy(&buf);
+    }
+
+    CxBuffer buf{};
+};
+
+static void expect_default_flush_config(CxBuffer *buf) {
+    EXPECT_EQ(buf->flush_blkmax, 0);
+    EXPECT_EQ(buf->flush_blksize, 4096);
+    EXPECT_EQ(buf->flush_threshold, SIZE_MAX);
+    EXPECT_EQ(buf->flush_func, nullptr);
+    EXPECT_EQ(buf->flush_target, nullptr);
+}
+
+TEST(BufferInit, WrapSpace) {
+    CxTestingAllocator alloc;
+    CxBuffer buf;
+    void *space = cxMalloc(&alloc, 16);
+    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_DEFAULT);
+    expect_default_flush_config(&buf);
+    EXPECT_EQ(buf.space, space);
+    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
+    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, 0);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_EQ(buf.capacity, 16);
+    EXPECT_EQ(buf.allocator, &alloc);
+    cxBufferDestroy(&buf);
+    EXPECT_FALSE(alloc.verify());
+    cxFree(&alloc, space);
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(BufferInit, WrapSpaceAutoExtend) {
+    CxTestingAllocator alloc;
+    CxBuffer buf;
+    void *space = cxMalloc(&alloc, 16);
+    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_AUTO_EXTEND);
+    expect_default_flush_config(&buf);
+    EXPECT_EQ(buf.space, space);
+    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, CX_BUFFER_AUTO_EXTEND);
+    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, 0);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_EQ(buf.capacity, 16);
+    EXPECT_EQ(buf.allocator, &alloc);
+    cxBufferDestroy(&buf);
+    EXPECT_FALSE(alloc.verify());
+    cxFree(&alloc, space);
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(BufferInit, WrapSpaceAutoFree) {
+    CxTestingAllocator alloc;
+    CxBuffer buf;
+    void *space = cxMalloc(&alloc, 16);
+    cxBufferInit(&buf, space, 16, &alloc, CX_BUFFER_FREE_CONTENTS);
+    expect_default_flush_config(&buf);
+    EXPECT_EQ(buf.space, space);
+    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
+    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, CX_BUFFER_FREE_CONTENTS);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_EQ(buf.capacity, 16);
+    EXPECT_EQ(buf.allocator, &alloc);
+    EXPECT_FALSE(alloc.verify());
+    cxBufferDestroy(&buf);
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(BufferInit, FreshSpace) {
+    CxTestingAllocator alloc;
+    CxBuffer buf;
+    cxBufferInit(&buf, nullptr, 8, &alloc, CX_BUFFER_DEFAULT);
+    expect_default_flush_config(&buf);
+    EXPECT_NE(buf.space, nullptr);
+    EXPECT_EQ(buf.flags & CX_BUFFER_AUTO_EXTEND, 0);
+    EXPECT_EQ(buf.flags & CX_BUFFER_FREE_CONTENTS, CX_BUFFER_FREE_CONTENTS);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(buf.allocator, &alloc);
+    EXPECT_FALSE(alloc.verify()); // space is still allocated
+    cxBufferDestroy(&buf);
+    EXPECT_TRUE(alloc.verify());
+}
+
+class BufferShiftFixture : public ::testing::Test {
+protected:
+    void SetUp() override {
+        ASSERT_TRUE(alloc.verify());
+        cxBufferInit(&buf, nullptr, 16, &alloc, CX_BUFFER_DEFAULT);
+        memcpy(buf.space, "test____________", 16);
+        buf.capacity = 8; // purposely pretend that the buffer has less capacity s.t. we can test beyond the range
+        buf.pos = 4;
+        buf.size = 4;
+    }
+
+    void TearDown() override {
+        cxBufferDestroy(&buf);
+        EXPECT_TRUE(alloc.verify());
+    }
+
+    CxTestingAllocator alloc;
+    CxBuffer buf{};
+};
+
+class BufferShiftLeft : public BufferShiftFixture {
+};
+
+TEST_F(BufferShiftLeft, Zero) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShiftLeft(&buf, 0);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 4);
+    EXPECT_EQ(buf.size, 4);
+    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
+}
+
+TEST_F(BufferShiftLeft, ZeroOffsetInterface) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShift(&buf, -0);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 4);
+    EXPECT_EQ(buf.size, 4);
+    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
+}
+
+TEST_F(BufferShiftLeft, Standard) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShiftLeft(&buf, 2);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 2);
+    EXPECT_EQ(buf.size, 2);
+    EXPECT_TRUE(memcmp(buf.space, "stst________", 8) == 0);
+}
+
+TEST_F(BufferShiftLeft, Overshift) {
+    ASSERT_LT(buf.pos, 6);
+    ASSERT_LT(buf.size, 6);
+    int ret = cxBufferShiftLeft(&buf, 6);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
+}
+
+TEST_F(BufferShiftLeft, OvershiftPosOnly) {
+    buf.pos = 2;
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShiftLeft(&buf, 3);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 1);
+    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
+}
+
+TEST_F(BufferShiftLeft, OffsetInterface) {
+    buf.pos = 3;
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShift(&buf, -2);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 1);
+    EXPECT_EQ(buf.size, 2);
+    EXPECT_TRUE(memcmp(buf.space, "stst________", 8) == 0);
+}
+
+class BufferShiftRight : public BufferShiftFixture {
+};
+
+TEST_F(BufferShiftRight, Zero) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShiftRight(&buf, 0);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 4);
+    EXPECT_EQ(buf.size, 4);
+    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
+}
+
+TEST_F(BufferShiftRight, ZeroOffsetInterface) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShift(&buf, +0);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 4);
+    EXPECT_EQ(buf.size, 4);
+    EXPECT_TRUE(memcmp(buf.space, "test________", 8) == 0);
+}
+
+TEST_F(BufferShiftRight, Standard) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShiftRight(&buf, 3);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 7);
+    EXPECT_EQ(buf.size, 7);
+    EXPECT_TRUE(memcmp(buf.space, "testest_____", 8) == 0);
+}
+
+TEST_F(BufferShiftRight, OvershiftDiscard) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    ASSERT_EQ(buf.capacity, 8);
+    int ret = cxBufferShiftRight(&buf, 6);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 8);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_TRUE(memcmp(buf.space, "test__te____", 8) == 0);
+}
+
+TEST_F(BufferShiftRight, OvershiftExtend) {
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    ASSERT_EQ(buf.capacity, 8);
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    int ret = cxBufferShiftRight(&buf, 6);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 10);
+    EXPECT_EQ(buf.size, 10);
+    EXPECT_GE(buf.capacity, 10);
+    EXPECT_TRUE(memcmp(buf.space, "test__test__", 8) == 0);
+}
+
+TEST_F(BufferShiftRight, OffsetInterface) {
+    buf.pos = 3;
+    ASSERT_EQ(buf.size, 4);
+    int ret = cxBufferShift(&buf, 2);
+    EXPECT_EQ(ret, 0);
+    EXPECT_EQ(buf.pos, 5);
+    EXPECT_EQ(buf.size, 6);
+    EXPECT_TRUE(memcmp(buf.space, "tetest______", 8) == 0);
+}
+
+TEST(BufferMinimumCapacity, Sufficient) {
+    CxTestingAllocator alloc;
+    auto space = cxMalloc(&alloc, 8);
+    CxBuffer buf;
+    cxBufferInit(&buf, space, 8, &alloc, CX_BUFFER_FREE_CONTENTS);
+    memcpy(space, "Testing", 8);
+    buf.size = 8;
+    cxBufferMinimumCapacity(&buf, 6);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_TRUE(memcmp(buf.space, "Testing", 8) == 0);
+    cxBufferDestroy(&buf);
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(BufferMinimumCapacity, Extend) {
+    CxTestingAllocator alloc;
+    auto space = cxMalloc(&alloc, 8);
+    CxBuffer buf;
+    cxBufferInit(&buf, space, 8, &alloc, CX_BUFFER_FREE_CONTENTS); // NO auto extend!
+    memcpy(space, "Testing", 8);
+    buf.size = 8;
+    cxBufferMinimumCapacity(&buf, 16);
+    EXPECT_EQ(buf.capacity, 16);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_TRUE(memcmp(buf.space, "Testing", 8) == 0);
+    cxBufferDestroy(&buf);
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(BufferClear, Test) {
+    char space[16];
+    strcpy(space, "clear test");
+    CxBuffer buf;
+    cxBufferInit(&buf, space, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
+    ASSERT_EQ(buf.size, 0);
+    // only clear the used part of the buffer
+    cxBufferClear(&buf);
+    EXPECT_EQ(memcmp(space, "clear test", 10), 0);
+    buf.size = 5;
+    buf.pos = 3;
+    cxBufferClear(&buf);
+    EXPECT_EQ(memcmp(space, "\0\0\0\0\0 test", 10), 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_EQ(buf.pos, 0);
+    cxBufferDestroy(&buf);
+}
+
+class BufferWrite : public ::testing::Test {
+protected:
+    CxBuffer buf{}, target{};
+
+    void SetUp() override {
+        cxBufferInit(&target, nullptr, 16, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
+        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
+        buf.capacity = 8; // artificially reduce capacity to check OOB writes
+        memset(buf.space, 0, 16);
+        memcpy(buf.space, "prep", 4);
+        buf.size = buf.pos = 4;
+    }
+
+    void TearDown() override {
+        cxBufferDestroy(&buf);
+        cxBufferDestroy(&target);
+    }
+
+    void enableFlushing() {
+        buf.flush_target = &target;
+        buf.flush_func = reinterpret_cast<cx_write_func>(cxBufferWrite);
+        buf.flush_blkmax = 1;
+    }
+};
+
+static size_t mock_write_limited_rate(
+        void const *ptr,
+        size_t size,
+        __attribute__((unused)) size_t nitems,
+        CxBuffer *buffer
+) {
+    // simulate limited target drain capacity
+    static bool full = false;
+    if (full) {
+        full = false;
+        return 0;
+    } else {
+        full = true;
+        return cxBufferWrite(ptr, size, nitems > 2 ? 2 : nitems, buffer);
+    }
+}
+
+TEST_F(BufferWrite, SizeOneFit) {
+    const char *data = "test";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferWrite(data, 1, 4, &buf);
+    EXPECT_EQ(written, 4);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_EQ(buf.pos, 8);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
+}
+
+TEST_F(BufferWrite, SizeOneDiscard) {
+    const char *data = "testing";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferWrite(data, 1, 7, &buf);
+    EXPECT_EQ(written, 4);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_EQ(buf.pos, 8);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "preptest\0", 9), 0);
+}
+
+TEST_F(BufferWrite, SizeOneExtend) {
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    const char *data = "testing";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferWrite(data, 1, 7, &buf);
+    EXPECT_EQ(written, 7);
+    EXPECT_EQ(buf.size, 11);
+    EXPECT_EQ(buf.pos, 11);
+    EXPECT_GE(buf.capacity, 11);
+    EXPECT_EQ(memcmp(buf.space, "preptesting", 11), 0);
+}
+
+TEST_F(BufferWrite, MultibyteFit) {
+    const char *data = "test";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferWrite(data, 2, 2, &buf);
+    EXPECT_EQ(written, 2);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_EQ(buf.pos, 8);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
+}
+
+TEST_F(BufferWrite, MultibyteDiscard) {
+    const char *data = "testing";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.size, 4);
+    buf.pos = 3;
+    size_t written = cxBufferWrite(data, 2, 4, &buf);
+    // remember: whole elements are discarded if they do not fit
+    EXPECT_EQ(written, 2);
+    EXPECT_EQ(buf.size, 7);
+    EXPECT_EQ(buf.pos, 7);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "pretest\0", 8), 0);
+}
+
+TEST_F(BufferWrite, MultibyteExtend) {
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    const char *data = "tester";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.size, 4);
+    buf.pos = 3;
+    size_t written = cxBufferWrite(data, 2, 3, &buf);
+    // remember: whole elements are discarded if they do not fit
+    EXPECT_EQ(written, 3);
+    EXPECT_EQ(buf.size, 9);
+    EXPECT_EQ(buf.pos, 9);
+    EXPECT_GE(buf.capacity, 9);
+    EXPECT_EQ(memcmp(buf.space, "pretester", 9), 0);
+}
+
+TEST_F(BufferWrite, PutcWrapperFit) {
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    int c = cxBufferPut(&buf, 0x200 | 'a');
+    EXPECT_EQ(c, 'a');
+    EXPECT_EQ(buf.size, 5);
+    EXPECT_EQ(buf.pos, 5);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "prepa\0", 6), 0);
+}
+
+TEST_F(BufferWrite, PutcWrapperDiscard) {
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.size, 4);
+    buf.pos = 8;
+    int c = cxBufferPut(&buf, 0x200 | 'a');
+    EXPECT_EQ(c, EOF);
+    EXPECT_EQ(buf.size, 4);
+    EXPECT_EQ(buf.pos, 8);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "prep\0\0\0\0\0", 9), 0);
+}
+
+TEST_F(BufferWrite, PutcWrapperExtend) {
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.size, 4);
+    buf.pos = 8;
+    int c = cxBufferPut(&buf, 0x200 | 'a');
+    EXPECT_EQ(c, 'a');
+    EXPECT_EQ(buf.size, 9);
+    EXPECT_EQ(buf.pos, 9);
+    EXPECT_GE(buf.capacity, 9);
+    EXPECT_EQ(memcmp(buf.space, "prep\0\0\0\0a", 9), 0);
+}
+
+TEST_F(BufferWrite, PutStringWrapperFit) {
+    const char *data = "test";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferPutString(&buf, data);
+    EXPECT_EQ(written, 4);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_EQ(buf.pos, 8);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "preptest", 8), 0);
+}
+
+TEST_F(BufferWrite, PutStringWrapperDiscard) {
+    const char *data = "testing";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferPutString(&buf, data);
+    EXPECT_EQ(written, 4);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_EQ(buf.pos, 8);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, "preptest\0", 9), 0);
+}
+
+TEST_F(BufferWrite, PutStringWrapperExtend) {
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    const char *data = "testing";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferPutString(&buf, data);
+    EXPECT_EQ(written, 7);
+    EXPECT_EQ(buf.size, 11);
+    EXPECT_EQ(buf.pos, 11);
+    EXPECT_GE(buf.capacity, 11);
+    EXPECT_EQ(memcmp(buf.space, "preptesting", 11), 0);
+}
+
+TEST_F(BufferWrite, MultOverflow) {
+    const char *data = "testing";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferWrite(data, 8, SIZE_MAX / 4, &buf);
+    EXPECT_EQ(written, 0);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(buf.pos, 4);
+    EXPECT_EQ(buf.size, 4);
+    EXPECT_EQ(memcmp(buf.space, "prep\0", 5), 0);
+}
+
+TEST_F(BufferWrite, MaxCapaOverflow) {
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    const char *data = "testing";
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    ASSERT_EQ(buf.size, 4);
+    size_t written = cxBufferWrite(data, 1, SIZE_MAX - 2, &buf);
+    EXPECT_EQ(written, 0);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(buf.pos, 4);
+    EXPECT_EQ(buf.size, 4);
+    EXPECT_EQ(memcmp(buf.space, "prep\0", 5), 0);
+}
+
+TEST_F(BufferWrite, OnlyOverwrite) {
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    ASSERT_EQ(buf.capacity, 8);
+    memcpy(buf.space, "preptest", 8);
+    buf.pos = 3;
+    buf.size = 8;
+    size_t written = cxBufferWrite("XXX", 2, 2, &buf);
+    EXPECT_EQ(written, 2);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(buf.size, 8);
+    EXPECT_EQ(buf.pos, 7);
+    EXPECT_EQ(memcmp(buf.space, "preXXX\0t", 8), 0);
+}
+
+TEST_F(BufferWrite, FlushAtCapacity) {
+    enableFlushing();
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    size_t written = cxBufferWrite("foo", 1, 3, &buf);
+    EXPECT_EQ(written, 3);
+    ASSERT_EQ(buf.pos, 7);
+    ASSERT_EQ(buf.size, 7);
+    ASSERT_EQ(target.pos, 0);
+    ASSERT_EQ(target.size, 0);
+    written = cxBufferWrite("hello", 1, 5, &buf);
+    EXPECT_EQ(written, 5);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(target.pos, 12);
+    ASSERT_EQ(target.size, 12);
+    EXPECT_EQ(memcmp(target.space, "prepfoohello", 12), 0);
+}
+
+TEST_F(BufferWrite, FlushAtThreshold) {
+    enableFlushing();
+    buf.flush_threshold = 12;
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    size_t written = cxBufferWrite("foobar", 1, 6, &buf);
+    EXPECT_EQ(written, 6);
+    ASSERT_EQ(buf.pos, 10);
+    ASSERT_EQ(buf.size, 10);
+    ASSERT_GE(buf.capacity, 10);
+    ASSERT_LE(buf.capacity, 12);
+    ASSERT_EQ(target.pos, 0);
+    ASSERT_EQ(target.size, 0);
+    written = cxBufferWrite("hello", 1, 5, &buf);
+    EXPECT_EQ(written, 5);
+    EXPECT_EQ(buf.pos, 0);
+    EXPECT_EQ(buf.size, 0);
+    EXPECT_LE(buf.capacity, 12);
+    EXPECT_EQ(target.pos, 15);
+    ASSERT_EQ(target.size, 15);
+    EXPECT_EQ(memcmp(target.space, "prepfoobarhello", 15), 0);
+}
+
+TEST_F(BufferWrite, FlushRateLimited) {
+    enableFlushing();
+    // limit the rate of the flush function and the capacity of the target
+    target.capacity = 16;
+    target.flags &= ~CX_BUFFER_AUTO_EXTEND;
+    buf.flush_func = (cx_write_func) mock_write_limited_rate;
+    ASSERT_EQ(buf.capacity, 8);
+    ASSERT_EQ(buf.pos, 4);
+    size_t written = cxBufferWrite("foo", 1, 3, &buf);
+    EXPECT_EQ(written, 3);
+    ASSERT_EQ(buf.pos, 7);
+    ASSERT_EQ(buf.size, 7);
+    ASSERT_EQ(target.pos, 0);
+    ASSERT_EQ(target.size, 0);
+    written = cxBufferWrite("hello, world!", 1, 13, &buf);
+    // " world!" fits into this buffer, the remaining stuff is flushed out
+    EXPECT_EQ(written, 13);
+    EXPECT_EQ(buf.pos, 7);
+    EXPECT_EQ(buf.size, 7);
+    EXPECT_EQ(buf.capacity, 8);
+    EXPECT_EQ(memcmp(buf.space, " world!", 7), 0);
+    EXPECT_EQ(target.pos, 13);
+    ASSERT_EQ(target.size, 13);
+    EXPECT_EQ(target.capacity, 16);
+    EXPECT_EQ(memcmp(target.space, "prepfoohello,", 13), 0);
+}
+
+class BufferSeek : public BufferFixture {
+};
+
+TEST_F(BufferSeek, SetZero) {
+    int result = cxBufferSeek(&buf, 0, SEEK_SET);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(buf.pos, 0);
+}
+
+TEST_F(BufferSeek, SetValid) {
+    int result = cxBufferSeek(&buf, 5, SEEK_SET);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(buf.pos, 5);
+}
+
+TEST_F(BufferSeek, SetInvalid) {
+    ASSERT_EQ(buf.pos, 3);
+    int result = cxBufferSeek(&buf, 6, SEEK_SET);
+    EXPECT_NE(result, 0);
+    EXPECT_EQ(buf.pos, 3);
+}
+
+TEST_F(BufferSeek, CurZero) {
+    ASSERT_EQ(buf.pos, 3);
+    int result = cxBufferSeek(&buf, 0, SEEK_CUR);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(buf.pos, 3);
+}
+
+TEST_F(BufferSeek, CurValidPositive) {
+    ASSERT_EQ(buf.pos, 3);
+    int result = cxBufferSeek(&buf, 2, SEEK_CUR);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(buf.pos, 5);
+}
+
+TEST_F(BufferSeek, CurValidNegative) {
+    ASSERT_EQ(buf.pos, 3);
+    int result = cxBufferSeek(&buf, -3, SEEK_CUR);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(buf.pos, 0);
+}
+
+TEST_F(BufferSeek, CurInvalidPositive) {
+    ASSERT_EQ(buf.pos, 3);
+    int result = cxBufferSeek(&buf, 3, SEEK_CUR);
+    EXPECT_NE(result, 0);
+    EXPECT_EQ(buf.pos, 3);
+}
+
+TEST_F(BufferSeek, CurInvalidNegative) {
+    ASSERT_EQ(buf.pos, 3);
+    int result = cxBufferSeek(&buf, -4, SEEK_CUR);
+    EXPECT_NE(result, 0);
+    EXPECT_EQ(buf.pos, 3);
+}
+
+TEST_F(BufferSeek, EndZero) {
+    ASSERT_EQ(buf.size, 6);
+    int result = cxBufferSeek(&buf, 0, SEEK_END);
+    // the (past-the-)end position is always invalid
+    EXPECT_NE(result, 0);
+    EXPECT_EQ(buf.pos, 3);
+}
+
+TEST_F(BufferSeek, EndValid) {
+    ASSERT_EQ(buf.size, 6);
+    int result = cxBufferSeek(&buf, -6, SEEK_END);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(buf.pos, 0);
+}
+
+TEST_F(BufferSeek, EndInvalid) {
+    ASSERT_EQ(buf.size, 6);
+    int result = cxBufferSeek(&buf, 1, SEEK_END);
+    EXPECT_NE(result, 0);
+    EXPECT_EQ(buf.pos, 3);
+}
+
+TEST_F(BufferSeek, WhenceInvalid) {
+    ASSERT_EQ(buf.size, 6);
+    ASSERT_EQ(buf.pos, 3);
+    int result = cxBufferSeek(&buf, 2, 9000);
+    EXPECT_NE(result, 0);
+    EXPECT_EQ(buf.size, 6);
+    EXPECT_EQ(buf.pos, 3);
+}
+
+class BufferEof : public BufferFixture {
+};
+
+TEST_F(BufferEof, Reached) {
+    buf.pos = buf.size;
+    EXPECT_TRUE(cxBufferEof(&buf));
+    buf.pos = buf.size - 1;
+    ASSERT_FALSE(cxBufferEof(&buf));
+    cxBufferPut(&buf, 'a');
+    EXPECT_TRUE(cxBufferEof(&buf));
+}
+
+TEST_F(BufferEof, NotReached) {
+    buf.pos = buf.size - 1;
+    EXPECT_FALSE(cxBufferEof(&buf));
+    buf.pos = 0;
+    cxBufferWrite("test", 1, 5, &buf);
+    EXPECT_FALSE(cxBufferEof(&buf));
+}
+
+class BufferRead : public ::testing::Test {
+protected:
+    CxBuffer buf{};
+
+    void SetUp() override {
+        cxBufferInit(&buf, nullptr, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
+        buf.capacity = 8; // artificially reduce capacity to check OOB writes
+        memset(buf.space, 0, 16);
+        memcpy(buf.space, "some data", 9);
+        buf.size = 9;
+    }
+
+    void TearDown() override {
+        cxBufferDestroy(&buf);
+    }
+};
+
+TEST_F(BufferRead, GetByte) {
+    buf.pos = 2;
+    EXPECT_EQ(cxBufferGet(&buf), 'm');
+    EXPECT_EQ(cxBufferGet(&buf), 'e');
+    EXPECT_EQ(cxBufferGet(&buf), ' ');
+    EXPECT_EQ(cxBufferGet(&buf), 'd');
+    EXPECT_EQ(buf.pos, 6);
+}
+
+TEST_F(BufferRead, GetEof) {
+    buf.pos = buf.size;
+    EXPECT_EQ(cxBufferGet(&buf), EOF);
+}
+
+TEST_F(BufferRead, ReadWithinBounds) {
+    buf.pos = 2;
+    char target[4];
+    auto read = cxBufferRead(&target, 1, 4, &buf);
+    ASSERT_EQ(read, 4);
+    EXPECT_EQ(memcmp(&target, "me d", 4), 0);
+    EXPECT_EQ(buf.pos, 6);
+}
+
+TEST_F(BufferRead, ReadOutOfBounds) {
+    buf.pos = 6;
+    char target[4];
+    auto read = cxBufferRead(&target, 1, 4, &buf);
+    ASSERT_EQ(read, 3);
+    EXPECT_EQ(memcmp(&target, "ata", 3), 0);
+    EXPECT_EQ(buf.pos, 9);
+}
+
+TEST_F(BufferRead, ReadOutOfBoundsMultibyte) {
+    buf.pos = 6;
+    char target[4];
+    target[2] = '\0';
+    auto read = cxBufferRead(&target, 2, 2, &buf);
+    ASSERT_EQ(read, 1);
+    EXPECT_EQ(memcmp(&target, "at\0", 3), 0);
+    EXPECT_EQ(buf.pos, 8);
+}
+
+TEST_F(BufferRead, ReadEof) {
+    buf.pos = 9;
+    char target[4];
+    auto read = cxBufferRead(&target, 1, 1, &buf);
+    ASSERT_EQ(read, 0);
+    EXPECT_EQ(buf.pos, 9);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_compare.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,127 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2022 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/compare.h"
+
+#include <gtest/gtest.h>
+
+template<typename T>
+static void test_compare(
+        int (*fnc)(
+                void const *,
+                void const *
+        )
+) {
+    auto m = std::numeric_limits<T>::max() / 400;
+    T x, y;
+
+    x = (std::is_signed_v<T> ? -3 : 3) * m;
+    y = 5 * m;
+    EXPECT_LT(fnc(&x, &y), 0);
+    EXPECT_GT(fnc(&y, &x), 0);
+
+    x = 120 * m;
+    y = 348 * m;
+    EXPECT_LT(fnc(&x, &y), 0);
+    EXPECT_GT(fnc(&y, &x), 0);
+
+    if constexpr (std::is_signed_v<T>) {
+        x = -120 * m;
+        y = -348 * m;
+        EXPECT_GT(fnc(&x, &y), 0);
+        EXPECT_LT(fnc(&y, &x), 0);
+    }
+
+    x = y;
+    EXPECT_EQ(fnc(&x, &y), 0);
+    EXPECT_EQ(fnc(&y, &x), 0);
+}
+
+TEST(Compare, Int) {
+    test_compare<int>(cx_cmp_int);
+}
+
+TEST(Compare, Longint) {
+    test_compare<long int>(cx_cmp_longint);
+}
+
+TEST(Compare, Longlong) {
+    test_compare<long long>(cx_cmp_longlong);
+}
+
+TEST(Compare, Int16) {
+    test_compare<int16_t>(cx_cmp_int16);
+}
+
+TEST(Compare, Int32) {
+    test_compare<int32_t>(cx_cmp_int32);
+}
+
+TEST(Compare, Int64) {
+    test_compare<int64_t>(cx_cmp_int64);
+}
+
+TEST(Compare, Uint) {
+    test_compare<unsigned int>(cx_cmp_uint);
+}
+
+TEST(Compare, Ulongint) {
+    test_compare<unsigned long int>(cx_cmp_ulongint);
+}
+
+TEST(Compare, Ulonglong) {
+    test_compare<unsigned long long>(cx_cmp_ulonglong);
+}
+
+TEST(Compare, Uint16) {
+    test_compare<uint16_t>(cx_cmp_uint16);
+}
+
+TEST(Compare, Uint32) {
+    test_compare<uint32_t>(cx_cmp_uint32);
+}
+
+TEST(Compare, Uint64) {
+    test_compare<uint64_t>(cx_cmp_uint64);
+}
+
+TEST(Compare, Float) {
+    test_compare<float>(cx_cmp_float);
+}
+
+TEST(Compare, Double) {
+    test_compare<double>(cx_cmp_double);
+}
+
+TEST(Compare, IntPtr) {
+    test_compare<intptr_t>(cx_cmp_intptr);
+}
+
+TEST(Compare, UintPtr) {
+    test_compare<uintptr_t>(cx_cmp_uintptr);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_hash_key.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,87 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/hash_key.h"
+
+#include <gtest/gtest.h>
+
+TEST(cx_hash_key, functions) {
+    auto str = "my key";
+    auto len = strlen(str);
+
+    auto str_key = cx_hash_key_str(str);
+    auto bytes_key = cx_hash_key_bytes(
+            reinterpret_cast<unsigned char const *>(str), len);
+    auto obj_key = cx_hash_key(
+            reinterpret_cast<void const *>(str), len);
+
+    EXPECT_EQ(str_key.hash, bytes_key.hash);
+    EXPECT_EQ(obj_key.hash, bytes_key.hash);
+    EXPECT_EQ(str_key.len, len);
+    EXPECT_EQ(bytes_key.len, len);
+    EXPECT_EQ(bytes_key.len, len);
+    EXPECT_EQ(str_key.data.cstr, str);
+    EXPECT_EQ(bytes_key.data.cbytes, reinterpret_cast<unsigned char const *>(str));
+    EXPECT_EQ(bytes_key.data.cobj, reinterpret_cast<void const *>(str));
+}
+
+TEST(cx_hash_key, empty_string) {
+    auto str = "";
+
+    auto str_key = cx_hash_key_str(str);
+    auto bytes_key = cx_hash_key_bytes(
+            reinterpret_cast<unsigned char const *>(str), 0);
+    auto obj_key = cx_hash_key(
+            reinterpret_cast<void const *>(str), 0);
+
+    EXPECT_EQ(bytes_key.hash, 4152238450u);
+    EXPECT_EQ(str_key.hash, 4152238450u);
+    EXPECT_EQ(obj_key.hash, 4152238450u);
+    EXPECT_EQ(str_key.len, 0);
+    EXPECT_EQ(bytes_key.len, 0);
+    EXPECT_EQ(bytes_key.len, 0);
+    EXPECT_EQ(str_key.data.cstr, str);
+    EXPECT_EQ(bytes_key.data.cbytes, reinterpret_cast<unsigned char const *>(str));
+    EXPECT_EQ(bytes_key.data.cobj, reinterpret_cast<void const *>(str));
+}
+
+TEST(cx_hash_key, null_ptr) {
+    auto str_key = cx_hash_key_str(nullptr);
+    auto bytes_key = cx_hash_key_bytes(nullptr, 0);
+    auto obj_key = cx_hash_key(nullptr, 0);
+
+    EXPECT_EQ(bytes_key.hash, 1574210520u);
+    EXPECT_EQ(str_key.hash, 1574210520u);
+    EXPECT_EQ(obj_key.hash, 1574210520u);
+    EXPECT_EQ(str_key.len, 0);
+    EXPECT_EQ(bytes_key.len, 0);
+    EXPECT_EQ(bytes_key.len, 0);
+    EXPECT_EQ(str_key.data.cstr, nullptr);
+    EXPECT_EQ(bytes_key.data.cbytes, nullptr);
+    EXPECT_EQ(bytes_key.data.cobj, nullptr);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_list.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,1077 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/linked_list.h"
+#include "cx/array_list.h"
+#include "cx/utils.h"
+#include "cx/compare.h"
+#include "util_allocator.h"
+
+#include <gtest/gtest.h>
+#include <array>
+#include <vector>
+#include <unordered_set>
+#include <algorithm>
+
+struct node {
+    node *next = nullptr;
+    node *prev = nullptr;
+    int data = 0;
+};
+
+const ptrdiff_t loc_prev = offsetof(struct node, prev);
+const ptrdiff_t loc_next = offsetof(struct node, next);
+const ptrdiff_t loc_data = offsetof(struct node, data);
+
+struct node_test_data {
+    node *begin = nullptr;
+
+    explicit node_test_data(node *begin) : begin(begin) {
+        auto n = begin;
+        while (n != nullptr) {
+            nodes.push_back(n);
+            n = n->next;
+        }
+    }
+
+    node_test_data(node_test_data &) = delete;
+
+    node_test_data(node_test_data &&) = default;
+
+    ~node_test_data() {
+        for (auto &&n: nodes) delete n;
+    }
+
+private:
+    std::vector<node *> nodes;
+};
+
+static node_test_data create_nodes_test_data(size_t len) {
+    if (len == 0) return node_test_data{nullptr};
+    auto begin = new node;
+    auto prev = begin;
+    for (size_t i = 1; i < len; i++) {
+        auto n = new node;
+        cx_linked_list_link(prev, n, loc_prev, loc_next);
+        prev = n;
+    }
+    return node_test_data{begin};
+}
+
+template<typename InputIter>
+static node_test_data create_nodes_test_data(
+        InputIter begin,
+        InputIter end
+) {
+    if (begin == end) return node_test_data{nullptr};
+    node *first = new node;
+    first->data = *begin;
+    node *prev = first;
+    begin++;
+    for (; begin != end; begin++) {
+        auto n = new node;
+        n->data = *begin;
+        cx_linked_list_link(prev, n, loc_prev, loc_next);
+        prev = n;
+    }
+    return node_test_data{first};
+}
+
+static node_test_data create_nodes_test_data(std::initializer_list<int> data) {
+    return create_nodes_test_data(data.begin(), data.end());
+}
+
+template<size_t N>
+struct int_test_data {
+    std::array<int, N> data;
+
+    int_test_data() {
+        cx_for_n (i, N) data[i] = ::rand(); // NOLINT(cert-msc50-cpp)
+    }
+};
+
+TEST(LinkedList_LowLevel, link_unlink) {
+    node a, b, c;
+
+    cx_linked_list_link(&a, &b, loc_prev, loc_next);
+    EXPECT_EQ(a.prev, nullptr);
+    EXPECT_EQ(a.next, &b);
+    EXPECT_EQ(b.prev, &a);
+    EXPECT_EQ(b.next, nullptr);
+
+    cx_linked_list_unlink(&a, &b, loc_prev, loc_next);
+    EXPECT_EQ(a.prev, nullptr);
+    EXPECT_EQ(a.next, nullptr);
+    EXPECT_EQ(b.prev, nullptr);
+    EXPECT_EQ(b.next, nullptr);
+
+    cx_linked_list_link(&b, &c, loc_prev, loc_next);
+    cx_linked_list_link(&a, &b, loc_prev, loc_next);
+    cx_linked_list_unlink(&b, &c, loc_prev, loc_next);
+    EXPECT_EQ(a.prev, nullptr);
+    EXPECT_EQ(a.next, &b);
+    EXPECT_EQ(b.prev, &a);
+    EXPECT_EQ(b.next, nullptr);
+    EXPECT_EQ(c.prev, nullptr);
+    EXPECT_EQ(c.next, nullptr);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_at) {
+    node a, b, c, d;
+    cx_linked_list_link(&a, &b, loc_prev, loc_next);
+    cx_linked_list_link(&b, &c, loc_prev, loc_next);
+    cx_linked_list_link(&c, &d, loc_prev, loc_next);
+
+    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 0), &a);
+    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 1), &b);
+    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 2), &c);
+    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 3), &d);
+    EXPECT_EQ(cx_linked_list_at(&a, 0, loc_next, 4), nullptr);
+
+    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_prev, 0), &a);
+    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 1), &b);
+    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 2), &c);
+    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 3), &d);
+    EXPECT_EQ(cx_linked_list_at(&b, 1, loc_next, 4), nullptr);
+
+    EXPECT_EQ(cx_linked_list_at(&d, 3, loc_prev, 0), &a);
+    EXPECT_EQ(cx_linked_list_at(&d, 3, loc_prev, 1), &b);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_find) {
+    auto testdata = create_nodes_test_data({2, 4, 6, 8});
+    auto list = testdata.begin;
+    int s;
+
+    s = 2;
+    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 0);
+    s = 4;
+    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 1);
+    s = 6;
+    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 2);
+    s = 8;
+    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 3);
+    s = 10;
+    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 4);
+    s = -2;
+    EXPECT_EQ(cx_linked_list_find(list, loc_next, loc_data, cx_cmp_int, &s), 4);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_compare) {
+    auto ta = create_nodes_test_data({2, 4, 6, 8});
+    auto tb = create_nodes_test_data({2, 4, 6});
+    auto tc = create_nodes_test_data({2, 4, 6, 9});
+    auto la = ta.begin, lb = tb.begin, lc = tc.begin;
+
+    EXPECT_GT(cx_linked_list_compare(la, lb, loc_next, loc_data, cx_cmp_int), 0);
+    EXPECT_LT(cx_linked_list_compare(lb, la, loc_next, loc_data, cx_cmp_int), 0);
+    EXPECT_GT(cx_linked_list_compare(lc, la, loc_next, loc_data, cx_cmp_int), 0);
+    EXPECT_LT(cx_linked_list_compare(la, lc, loc_next, loc_data, cx_cmp_int), 0);
+    EXPECT_EQ(cx_linked_list_compare(la, la, loc_next, loc_data, cx_cmp_int), 0);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_add) {
+    // test with begin, end / prev, next
+    {
+        node nodes[4];
+        void *begin = nullptr, *end = nullptr;
+
+        cx_linked_list_add(&begin, &end, loc_prev, loc_next, &nodes[0]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[0]);
+        EXPECT_EQ(nodes[0].prev, nullptr);
+        EXPECT_EQ(nodes[0].next, nullptr);
+
+        cx_linked_list_add(&begin, &end, loc_prev, loc_next, &nodes[1]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[1]);
+        EXPECT_EQ(nodes[0].next, &nodes[1]);
+        EXPECT_EQ(nodes[1].prev, &nodes[0]);
+    }
+
+    // test with begin only / prev, next
+    {
+        node nodes[4];
+        void *begin = nullptr;
+
+        cx_linked_list_add(&begin, nullptr, loc_prev, loc_next, &nodes[0]);
+        EXPECT_EQ(begin, &nodes[0]);
+        cx_linked_list_add(&begin, nullptr, loc_prev, loc_next, &nodes[1]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(nodes[0].next, &nodes[1]);
+        EXPECT_EQ(nodes[1].prev, &nodes[0]);
+
+        cx_linked_list_add(&begin, nullptr, loc_prev, loc_next, &nodes[2]);
+        EXPECT_EQ(nodes[1].next, &nodes[2]);
+        EXPECT_EQ(nodes[2].prev, &nodes[1]);
+    }
+
+    // test with end only / prev, next
+    {
+        node nodes[4];
+        void *end = nullptr;
+
+        cx_linked_list_add(nullptr, &end, loc_prev, loc_next, &nodes[0]);
+        EXPECT_EQ(end, &nodes[0]);
+        cx_linked_list_add(nullptr, &end, loc_prev, loc_next, &nodes[1]);
+        EXPECT_EQ(end, &nodes[1]);
+        EXPECT_EQ(nodes[0].next, &nodes[1]);
+        EXPECT_EQ(nodes[1].prev, &nodes[0]);
+
+        cx_linked_list_add(nullptr, &end, loc_prev, loc_next, &nodes[2]);
+        EXPECT_EQ(end, &nodes[2]);
+        EXPECT_EQ(nodes[1].next, &nodes[2]);
+        EXPECT_EQ(nodes[2].prev, &nodes[1]);
+    }
+
+    // test with begin, end / next
+    {
+        node nodes[4];
+        void *begin = nullptr, *end = nullptr;
+
+        cx_linked_list_add(&begin, &end, -1, loc_next, &nodes[0]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[0]);
+        cx_linked_list_add(&begin, &end, -1, loc_next, &nodes[1]);
+        EXPECT_EQ(end, &nodes[1]);
+        EXPECT_EQ(nodes[0].next, &nodes[1]);
+        EXPECT_EQ(nodes[1].prev, nullptr);
+    }
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_prepend) {
+    // test with begin, end / prev, next
+    {
+        node nodes[4];
+        void *begin = nullptr, *end = nullptr;
+
+        cx_linked_list_prepend(&begin, &end, loc_prev, loc_next, &nodes[0]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[0]);
+        EXPECT_EQ(nodes[0].prev, nullptr);
+        EXPECT_EQ(nodes[0].next, nullptr);
+
+        cx_linked_list_prepend(&begin, &end, loc_prev, loc_next, &nodes[1]);
+        EXPECT_EQ(begin, &nodes[1]);
+        EXPECT_EQ(end, &nodes[0]);
+        EXPECT_EQ(nodes[1].next, &nodes[0]);
+        EXPECT_EQ(nodes[0].prev, &nodes[1]);
+    }
+
+    // test with begin only / prev, next
+    {
+        node nodes[4];
+        void *begin = nullptr;
+
+        cx_linked_list_prepend(&begin, nullptr, loc_prev, loc_next, &nodes[0]);
+        EXPECT_EQ(begin, &nodes[0]);
+        cx_linked_list_prepend(&begin, nullptr, loc_prev, loc_next, &nodes[1]);
+        EXPECT_EQ(begin, &nodes[1]);
+        EXPECT_EQ(nodes[1].next, &nodes[0]);
+        EXPECT_EQ(nodes[0].prev, &nodes[1]);
+
+        cx_linked_list_prepend(&begin, nullptr, loc_prev, loc_next, &nodes[2]);
+        EXPECT_EQ(begin, &nodes[2]);
+        EXPECT_EQ(nodes[2].next, &nodes[1]);
+        EXPECT_EQ(nodes[1].prev, &nodes[2]);
+    }
+
+    // test with end only / prev, next
+    {
+        node nodes[4];
+        void *end = nullptr;
+
+        cx_linked_list_prepend(nullptr, &end, loc_prev, loc_next, &nodes[0]);
+        EXPECT_EQ(end, &nodes[0]);
+        cx_linked_list_prepend(nullptr, &end, loc_prev, loc_next, &nodes[1]);
+        EXPECT_EQ(end, &nodes[0]);
+        EXPECT_EQ(nodes[1].next, &nodes[0]);
+        EXPECT_EQ(nodes[0].prev, &nodes[1]);
+
+        cx_linked_list_prepend(nullptr, &end, loc_prev, loc_next, &nodes[2]);
+        EXPECT_EQ(end, &nodes[0]);
+        EXPECT_EQ(nodes[2].next, &nodes[1]);
+        EXPECT_EQ(nodes[1].prev, &nodes[2]);
+    }
+
+    // test with begin, end / next
+    {
+        node nodes[4];
+        void *begin = nullptr, *end = nullptr;
+
+        cx_linked_list_prepend(&begin, &end, -1, loc_next, &nodes[0]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[0]);
+        cx_linked_list_prepend(&begin, &end, -1, loc_next, &nodes[1]);
+        cx_linked_list_prepend(&begin, &end, -1, loc_next, &nodes[2]);
+        EXPECT_EQ(begin, &nodes[2]);
+        EXPECT_EQ(end, &nodes[0]);
+        EXPECT_EQ(nodes[1].next, &nodes[0]);
+        EXPECT_EQ(nodes[2].next, &nodes[1]);
+        EXPECT_EQ(nodes[1].prev, nullptr);
+        EXPECT_EQ(nodes[0].prev, nullptr);
+    }
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_insert) {
+    // insert mid list
+    {
+        node nodes[4];
+        void *begin = &nodes[0], *end = &nodes[2];
+
+        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
+
+        cx_linked_list_insert(&begin, &end, loc_prev, loc_next, &nodes[1], &nodes[3]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[2]);
+        EXPECT_EQ(nodes[1].next, &nodes[3]);
+        EXPECT_EQ(nodes[2].prev, &nodes[3]);
+        EXPECT_EQ(nodes[3].prev, &nodes[1]);
+        EXPECT_EQ(nodes[3].next, &nodes[2]);
+    }
+
+    // insert end
+    {
+        node nodes[4];
+        void *begin = &nodes[0], *end = &nodes[2];
+
+        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
+
+        cx_linked_list_insert(&begin, &end, loc_prev, loc_next, &nodes[2], &nodes[3]);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[3]);
+        EXPECT_EQ(nodes[2].next, &nodes[3]);
+        EXPECT_EQ(nodes[3].prev, &nodes[2]);
+        EXPECT_EQ(nodes[3].next, nullptr);
+    }
+
+    // insert begin
+    {
+        node nodes[4];
+        void *begin = &nodes[0], *end = &nodes[2];
+
+        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
+
+        cx_linked_list_insert(&begin, &end, loc_prev, loc_next, nullptr, &nodes[3]);
+        EXPECT_EQ(begin, &nodes[3]);
+        EXPECT_EQ(end, &nodes[2]);
+        EXPECT_EQ(nodes[0].prev, &nodes[3]);
+        EXPECT_EQ(nodes[3].prev, nullptr);
+        EXPECT_EQ(nodes[3].next, &nodes[0]);
+    }
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_insert_chain) {
+    // insert mid list
+    {
+        node nodes[5];
+        void *begin = &nodes[0], *end = &nodes[2];
+
+        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[3], &nodes[4], loc_prev, loc_next);
+
+        cx_linked_list_insert_chain(&begin, &end, loc_prev, loc_next, &nodes[1], &nodes[3], nullptr);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[2]);
+        EXPECT_EQ(nodes[1].next, &nodes[3]);
+        EXPECT_EQ(nodes[2].prev, &nodes[4]);
+        EXPECT_EQ(nodes[3].prev, &nodes[1]);
+        EXPECT_EQ(nodes[4].next, &nodes[2]);
+    }
+
+    // insert end
+    {
+        node nodes[5];
+        void *begin = &nodes[0], *end = &nodes[2];
+
+        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[3], &nodes[4], loc_prev, loc_next);
+
+        cx_linked_list_insert_chain(&begin, &end, loc_prev, loc_next, &nodes[2], &nodes[3], nullptr);
+        EXPECT_EQ(begin, &nodes[0]);
+        EXPECT_EQ(end, &nodes[4]);
+        EXPECT_EQ(nodes[2].next, &nodes[3]);
+        EXPECT_EQ(nodes[3].prev, &nodes[2]);
+        EXPECT_EQ(nodes[4].next, nullptr);
+    }
+
+    // insert begin
+    {
+        node nodes[5];
+        void *begin = &nodes[0], *end = &nodes[2];
+
+        cx_linked_list_link(&nodes[0], &nodes[1], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[1], &nodes[2], loc_prev, loc_next);
+        cx_linked_list_link(&nodes[3], &nodes[4], loc_prev, loc_next);
+
+        cx_linked_list_insert_chain(&begin, &end, loc_prev, loc_next, nullptr, &nodes[3], nullptr);
+        EXPECT_EQ(begin, &nodes[3]);
+        EXPECT_EQ(end, &nodes[2]);
+        EXPECT_EQ(nodes[0].prev, &nodes[4]);
+        EXPECT_EQ(nodes[3].prev, nullptr);
+        EXPECT_EQ(nodes[4].next, &nodes[0]);
+    }
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_first) {
+    auto testdata = create_nodes_test_data(3);
+    auto begin = testdata.begin;
+    EXPECT_EQ(cx_linked_list_first(begin, loc_prev), begin);
+    EXPECT_EQ(cx_linked_list_first(begin->next, loc_prev), begin);
+    EXPECT_EQ(cx_linked_list_first(begin->next->next, loc_prev), begin);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_last) {
+    auto testdata = create_nodes_test_data(3);
+    auto begin = testdata.begin;
+    auto end = begin->next->next;
+    EXPECT_EQ(cx_linked_list_last(begin, loc_next), end);
+    EXPECT_EQ(cx_linked_list_last(begin->next, loc_next), end);
+    EXPECT_EQ(cx_linked_list_last(begin->next->next, loc_next), end);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_prev) {
+    auto testdata = create_nodes_test_data(3);
+    auto begin = testdata.begin;
+    EXPECT_EQ(cx_linked_list_prev(begin, loc_next, begin), nullptr);
+    EXPECT_EQ(cx_linked_list_prev(begin, loc_next, begin->next), begin);
+    EXPECT_EQ(cx_linked_list_prev(begin, loc_next, begin->next->next), begin->next);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_remove) {
+    auto testdata = create_nodes_test_data({2, 4, 6});
+    auto begin = reinterpret_cast<void *>(testdata.begin);
+    auto first = testdata.begin;
+    auto second = first->next;
+    auto third = second->next;
+    auto end = reinterpret_cast<void *>(third);
+
+    cx_linked_list_remove(&begin, &end, loc_prev, loc_next, second);
+    EXPECT_EQ(begin, first);
+    EXPECT_EQ(end, third);
+    EXPECT_EQ(first->prev, nullptr);
+    EXPECT_EQ(first->next, third);
+    EXPECT_EQ(third->prev, first);
+    EXPECT_EQ(third->next, nullptr);
+
+    cx_linked_list_remove(&begin, &end, loc_prev, loc_next, third);
+    EXPECT_EQ(begin, first);
+    EXPECT_EQ(end, first);
+    EXPECT_EQ(first->prev, nullptr);
+    EXPECT_EQ(first->next, nullptr);
+
+    cx_linked_list_remove(&begin, &end, loc_prev, loc_next, first);
+    EXPECT_EQ(begin, nullptr);
+    EXPECT_EQ(end, nullptr);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_size) {
+    EXPECT_EQ(cx_linked_list_size(nullptr, loc_next), 0);
+
+    {
+        auto testdata = create_nodes_test_data(5);
+        EXPECT_EQ(cx_linked_list_size(testdata.begin, loc_next), 5);
+    }
+
+    {
+        auto testdata = create_nodes_test_data(13);
+        EXPECT_EQ(cx_linked_list_size(testdata.begin, loc_next), 13);
+    }
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_sort) {
+    int_test_data<1500> testdata;
+    std::array<int, 1500> sorted{};
+    std::partial_sort_copy(testdata.data.begin(), testdata.data.end(), sorted.begin(), sorted.end());
+
+    auto scrambled = create_nodes_test_data(testdata.data.begin(), testdata.data.end());
+    void *begin = scrambled.begin;
+    void *end = cx_linked_list_last(begin, loc_next);
+
+    cx_linked_list_sort(&begin, &end, loc_prev, loc_next, loc_data, cx_cmp_int);
+
+    node *check = reinterpret_cast<node *>(begin);
+    node *check_last = nullptr;
+    cx_for_n (i, sorted.size()) {
+        EXPECT_EQ(check->data, sorted[i]);
+        EXPECT_EQ(check->prev, check_last);
+        if (i < sorted.size() - 1) {
+            ASSERT_NE(check->next, nullptr);
+        }
+        check_last = check;
+        check = check->next;
+    }
+    EXPECT_EQ(check, nullptr);
+    EXPECT_EQ(end, check_last);
+}
+
+TEST(LinkedList_LowLevel, cx_linked_list_reverse) {
+    auto testdata = create_nodes_test_data({2, 4, 6, 8});
+    auto expected = create_nodes_test_data({8, 6, 4, 2});
+
+    auto begin = reinterpret_cast<void *>(testdata.begin);
+    auto end = cx_linked_list_last(begin, loc_next);
+    auto orig_begin = begin, orig_end = end;
+
+    cx_linked_list_reverse(&begin, &end, loc_prev, loc_next);
+    EXPECT_EQ(end, orig_begin);
+    EXPECT_EQ(begin, orig_end);
+    EXPECT_EQ(cx_linked_list_compare(begin, expected.begin, loc_next, loc_data, cx_cmp_int), 0);
+}
+
+class HighLevelTest : public ::testing::Test {
+    mutable std::unordered_set<CxList *> lists;
+protected:
+    CxTestingAllocator testingAllocator;
+
+    void TearDown() override {
+        for (auto &&l: lists) cxListDestroy(l);
+        EXPECT_TRUE(testingAllocator.verify());
+    }
+
+    static constexpr size_t testdata_len = 250;
+    int_test_data<testdata_len> testdata;
+
+    auto autofree(CxList *list) const -> CxList * {
+        if (list != nullptr) lists.insert(list);
+        return list;
+    }
+
+    auto linkedListFromTestData() const -> CxList * {
+        auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
+        cxListAddArray(list, testdata.data.data(), testdata_len);
+        return list;
+    }
+
+    auto pointerLinkedListFromTestData() const -> CxList * {
+        auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
+        cxListStorePointers(list);
+        // note: cannot use cxListAddArray() because we don't have a list of pointers
+        cx_for_n(i, testdata_len) cxListAdd(list, &testdata.data[i]);
+        return list;
+    }
+
+    auto arrayListFromTestData() const -> CxList * {
+        auto list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), testdata_len));
+        cxListAddArray(list, testdata.data.data(), testdata_len);
+        return list;
+    }
+
+    void verifyCreate(CxList *list) const {
+        EXPECT_EQ(list->content_destructor_type, CX_DESTRUCTOR_NONE);
+        EXPECT_EQ(list->size, 0);
+        EXPECT_EQ(list->allocator, &testingAllocator);
+        EXPECT_EQ(list->cmpfunc, cx_cmp_int);
+    }
+
+    void verifyAdd(
+            CxList *list,
+            bool as_pointer
+    ) {
+        auto len = testdata_len;
+        cx_for_n (i, len) EXPECT_EQ(cxListAdd(list, &testdata.data[i]), 0);
+        EXPECT_EQ(list->size, len);
+        EXPECT_GE(list->capacity, list->size);
+        cx_for_n (i, len) EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i]);
+        cx_for_n (i, len) ++testdata.data[i];
+        if (as_pointer) {
+            cx_for_n (i, len) EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i]);
+        } else {
+            cx_for_n (i, len) EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i] - 1);
+        }
+    }
+
+    static void verifyInsert(CxList *list) {
+        int a = 5, b = 47, c = 13, d = 42;
+
+        EXPECT_NE(cxListInsert(list, 1, &a), 0);
+        EXPECT_EQ(list->size, 0);
+        EXPECT_EQ(cxListInsert(list, 0, &a), 0);
+        EXPECT_EQ(list->size, 1);
+        EXPECT_EQ(cxListInsert(list, 0, &b), 0);
+        EXPECT_EQ(list->size, 2);
+        EXPECT_EQ(cxListInsert(list, 1, &c), 0);
+        EXPECT_EQ(list->size, 3);
+        EXPECT_EQ(cxListInsert(list, 3, &d), 0);
+
+        ASSERT_EQ(list->size, 4);
+        EXPECT_GE(list->capacity, list->size);
+
+        EXPECT_EQ(*(int *) cxListAt(list, 0), 47);
+        EXPECT_EQ(*(int *) cxListAt(list, 1), 13);
+        EXPECT_EQ(*(int *) cxListAt(list, 2), 5);
+        EXPECT_EQ(*(int *) cxListAt(list, 3), 42);
+    }
+
+    static void verifyInsertArray(
+            CxList *list,
+            bool pointers = false
+    ) {
+        int a[5] = {5, 47, 11, 13, 42};
+        int b[5] = {9, 18, 72, 50, 7};
+        int *aptr[5];
+        int *bptr[5];
+        cx_for_n(i, 5) {
+            aptr[i] = &a[i];
+            bptr[i] = &b[i];
+        }
+
+        size_t inserted;
+
+        if (pointers) {
+            inserted = cxListInsertArray(list, 0, aptr, 5);
+        } else {
+            inserted = cxListInsertArray(list, 0, a, 5);
+        }
+        EXPECT_EQ(inserted, 5);
+        EXPECT_EQ(*(int *) cxListAt(list, 0), 5);
+        EXPECT_EQ(*(int *) cxListAt(list, 1), 47);
+        EXPECT_EQ(*(int *) cxListAt(list, 2), 11);
+        EXPECT_EQ(*(int *) cxListAt(list, 3), 13);
+        EXPECT_EQ(*(int *) cxListAt(list, 4), 42);
+        if (pointers) {
+            inserted = cxListInsertArray(list, 3, bptr, 5);
+        } else {
+            inserted = cxListInsertArray(list, 3, b, 5);
+        }
+        EXPECT_EQ(inserted, 5);
+        EXPECT_EQ(*(int *) cxListAt(list, 0), 5);
+        EXPECT_EQ(*(int *) cxListAt(list, 1), 47);
+        EXPECT_EQ(*(int *) cxListAt(list, 2), 11);
+        EXPECT_EQ(*(int *) cxListAt(list, 3), 9);
+        EXPECT_EQ(*(int *) cxListAt(list, 4), 18);
+        EXPECT_EQ(*(int *) cxListAt(list, 5), 72);
+        EXPECT_EQ(*(int *) cxListAt(list, 6), 50);
+        EXPECT_EQ(*(int *) cxListAt(list, 7), 7);
+        EXPECT_EQ(*(int *) cxListAt(list, 8), 13);
+        EXPECT_EQ(*(int *) cxListAt(list, 9), 42);
+    }
+
+    void verifyRemove(CxList *list) const {
+        EXPECT_EQ(cxListRemove(list, 2), 0);
+        EXPECT_EQ(cxListRemove(list, 4), 0);
+        EXPECT_EQ(list->size, testdata_len - 2);
+        EXPECT_GE(list->capacity, list->size);
+        EXPECT_EQ(*(int *) cxListAt(list, 0), testdata.data[0]);
+        EXPECT_EQ(*(int *) cxListAt(list, 1), testdata.data[1]);
+        EXPECT_EQ(*(int *) cxListAt(list, 2), testdata.data[3]);
+        EXPECT_EQ(*(int *) cxListAt(list, 3), testdata.data[4]);
+        EXPECT_EQ(*(int *) cxListAt(list, 4), testdata.data[6]);
+
+        EXPECT_EQ(cxListRemove(list, 0), 0);
+        EXPECT_EQ(list->size, testdata_len - 3);
+        EXPECT_GE(list->capacity, list->size);
+        EXPECT_EQ(*(int *) cxListAt(list, 0), testdata.data[1]);
+        EXPECT_EQ(*(int *) cxListAt(list, 1), testdata.data[3]);
+
+        EXPECT_NE(cxListRemove(list, testdata_len), 0);
+    }
+
+    void verifyAt(CxList *list) const {
+        auto len = testdata_len;
+        EXPECT_EQ(list->size, len);
+        cx_for_n (i, len) {
+            EXPECT_EQ(*(int *) cxListAt(list, i), testdata.data[i]);
+        }
+        EXPECT_EQ(cxListAt(list, list->size), nullptr);
+    }
+
+    void verifyFind(CxList *list) const {
+        cx_for_n (attempt, 25) {
+            size_t exp = rand() % testdata_len; // NOLINT(cert-msc50-cpp)
+            int val = testdata.data[exp];
+            // randomly picked number could occur earlier in list - find first position
+            cx_for_n (i, exp) {
+                if (testdata.data[i] == val) {
+                    exp = i;
+                    break;
+                }
+            }
+            EXPECT_EQ(cxListFind(list, &val), exp);
+        }
+    }
+
+    void verifySort(CxList *list) const {
+        std::array<int, testdata_len> expected{};
+        std::partial_sort_copy(testdata.data.begin(), testdata.data.end(), expected.begin(), expected.end());
+        cxListSort(list);
+        cx_for_n (i, testdata_len) ASSERT_EQ(*(int *) cxListAt(list, i), expected[i]);
+    }
+
+    void verifyIterator(CxList *list) const {
+        int i = 0;
+        auto iter = cxListBeginMut(list);
+        cx_foreach(int*, x, iter) {
+            ASSERT_EQ(iter.index, (size_t) (i + 1) / 2);
+            ASSERT_EQ(*x, testdata.data[i]);
+            if (i % 2 == 1) cxIteratorFlagRemoval(iter);
+            i++;
+        }
+        auto len = testdata_len;
+        EXPECT_EQ(i, len);
+        ASSERT_EQ(list->size, len / 2);
+        cx_for_n(j, len / 2) ASSERT_EQ(*(int *) cxListAt(list, j), testdata.data[j * 2]);
+    }
+
+    static void verifyInsertViaIterator(CxList *list) {
+        int newdata[] = {10, 20, 30, 40, 50};
+
+        auto iter = cxListMutIterator(list, 2);
+        EXPECT_TRUE(cxIteratorValid(iter));
+        EXPECT_EQ(iter.index, 2);
+        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 2);
+        cxListInsertAfter(&iter, &newdata[0]);
+        EXPECT_TRUE(cxIteratorValid(iter));
+        EXPECT_EQ(iter.index, 2);
+        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 2);
+        cxListInsertBefore(&iter, &newdata[1]);
+        EXPECT_TRUE(cxIteratorValid(iter));
+        EXPECT_EQ(iter.index, 3);
+        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 2);
+
+        iter = cxListBeginMut(list);
+        cxListInsertBefore(&iter, &newdata[2]);
+        EXPECT_TRUE(cxIteratorValid(iter));
+        EXPECT_EQ(iter.index, 1);
+        EXPECT_EQ(*(int *) cxIteratorCurrent(iter), 0);
+        iter = cxListMutIterator(list, list->size);
+        cxListInsertBefore(&iter, &newdata[3]);
+        EXPECT_FALSE(cxIteratorValid(iter));
+        EXPECT_EQ(iter.index, 9);
+        iter = cxListMutIterator(list, list->size);
+        cxListInsertAfter(&iter, &newdata[4]);
+        EXPECT_FALSE(cxIteratorValid(iter));
+        EXPECT_EQ(iter.index, 10);
+
+        int expdata[] = {30, 0, 1, 20, 2, 10, 3, 4, 40, 50};
+        cx_for_n (j, 10) EXPECT_EQ(*(int *) cxListAt(list, j), expdata[j]);
+    }
+
+    void verifyReverse(CxList *list) const {
+        cxListReverse(list);
+        cx_for_n(i, testdata_len) {
+            ASSERT_EQ(*(int *) cxListAt(list, i), testdata.data[testdata_len - 1 - i]);
+        }
+    }
+
+    static void verifyCompare(
+            CxList *left,
+            CxList *right
+    ) {
+        EXPECT_EQ(cxListCompare(left, right), 0);
+        int x = 42;
+        cxListAdd(left, &x);
+        ASSERT_GT(left->size, right->size);
+        EXPECT_GT(cxListCompare(left, right), 0);
+        EXPECT_LT(cxListCompare(right, left), 0);
+        cxListAdd(right, &x);
+        ASSERT_EQ(left->size, right->size);
+        EXPECT_EQ(cxListCompare(left, right), 0);
+        int a = 5, b = 10;
+        cxListInsert(left, 15, &a);
+        cxListInsert(right, 15, &b);
+        ASSERT_EQ(left->size, right->size);
+        EXPECT_LT(cxListCompare(left, right), 0);
+        EXPECT_GT(cxListCompare(right, left), 0);
+        *(int *) cxListAt(left, 15) = 10;
+        EXPECT_EQ(cxListCompare(left, right), 0);
+    }
+};
+
+class LinkedList : public HighLevelTest {
+};
+
+class PointerLinkedList : public HighLevelTest {
+};
+
+class ArrayList : public HighLevelTest {
+};
+
+TEST_F(PointerLinkedList, cxListStorePointers) {
+    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, 47));
+    EXPECT_FALSE(cxListIsStoringPointers(list));
+    cxListStorePointers(list);
+    EXPECT_EQ(list->itemsize, sizeof(void *));
+    EXPECT_NE(list->cl, nullptr);
+    EXPECT_NE(list->climpl, nullptr);
+    EXPECT_TRUE(cxListIsStoringPointers(list));
+    cxListStoreObjects(list);
+    EXPECT_NE(list->cl, nullptr);
+    EXPECT_EQ(list->climpl, nullptr);
+    EXPECT_FALSE(cxListIsStoringPointers(list));
+}
+
+TEST_F(LinkedList, cxLinkedListCreate) {
+    CxList *list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
+    ASSERT_NE(list, nullptr);
+    EXPECT_EQ(list->itemsize, sizeof(int));
+    EXPECT_EQ(list->capacity, (size_t) -1);
+    verifyCreate(list);
+}
+
+TEST_F(ArrayList, cxArrayListCreate) {
+    CxList *list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 8));
+    ASSERT_NE(list, nullptr);
+    EXPECT_EQ(list->itemsize, sizeof(int));
+    EXPECT_EQ(list->capacity, 8);
+    verifyCreate(list);
+}
+
+TEST_F(LinkedList, cxListAdd) {
+    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
+    verifyAdd(list, false);
+}
+
+TEST_F(PointerLinkedList, cxListAdd) {
+    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
+    cxListStorePointers(list);
+    verifyAdd(list, true);
+}
+
+TEST_F(ArrayList, cxListAdd) {
+    auto list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 8));
+    verifyAdd(list, false);
+}
+
+TEST_F(LinkedList, cxListInsert) {
+    verifyInsert(autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int))));
+}
+
+TEST_F(PointerLinkedList, cxListInsert) {
+    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
+    cxListStorePointers(list);
+    verifyInsert(list);
+}
+
+TEST_F(ArrayList, cxListInsert) {
+    verifyInsert(autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 2)));
+}
+
+TEST_F(LinkedList, cxListInsertArray) {
+    verifyInsertArray(autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int))));
+}
+
+TEST_F(PointerLinkedList, cxListInsertArray) {
+    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
+    cxListStorePointers(list);
+    verifyInsertArray(list, true);
+}
+
+TEST_F(ArrayList, cxListInsertArray) {
+    verifyInsertArray(autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 4)));
+}
+
+TEST_F(LinkedList, cxListRemove) {
+    verifyRemove(linkedListFromTestData());
+}
+
+TEST_F(PointerLinkedList, cxListRemove) {
+    verifyRemove(pointerLinkedListFromTestData());
+}
+
+TEST_F(ArrayList, cxListRemove) {
+    verifyRemove(arrayListFromTestData());
+}
+
+TEST_F(LinkedList, cxListAt) {
+    verifyAt(linkedListFromTestData());
+}
+
+TEST_F(PointerLinkedList, cxListAt) {
+    verifyAt(pointerLinkedListFromTestData());
+}
+
+TEST_F(ArrayList, cxListAt) {
+    verifyAt(arrayListFromTestData());
+}
+
+TEST_F(LinkedList, cxListFind) {
+    verifyFind(linkedListFromTestData());
+}
+
+TEST_F(PointerLinkedList, cxListFind) {
+    verifyFind(pointerLinkedListFromTestData());
+}
+
+TEST_F(ArrayList, cxListFind) {
+    verifyFind(arrayListFromTestData());
+}
+
+TEST_F(LinkedList, cxListSort) {
+    verifySort(linkedListFromTestData());
+}
+
+TEST_F(PointerLinkedList, cxListSort) {
+    verifySort(pointerLinkedListFromTestData());
+}
+
+TEST_F(ArrayList, cxListSort) {
+    verifySort(arrayListFromTestData());
+}
+
+TEST_F(LinkedList, Iterator) {
+    verifyIterator(linkedListFromTestData());
+}
+
+TEST_F(PointerLinkedList, Iterator) {
+    verifyIterator(pointerLinkedListFromTestData());
+}
+
+TEST_F(ArrayList, Iterator) {
+    verifyIterator(arrayListFromTestData());
+}
+
+TEST_F(LinkedList, InsertViaIterator) {
+    int fivenums[] = {0, 1, 2, 3, 4, 5};
+    CxList *list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int)));
+    cxListAddArray(list, fivenums, 5);
+    verifyInsertViaIterator(list);
+}
+
+TEST_F(PointerLinkedList, InsertViaIterator) {
+    int fivenums[] = {0, 1, 2, 3, 4, 5};
+    auto list = autofree(cxLinkedListCreate(&testingAllocator, cx_cmp_int, sizeof(int *)));
+    cxListStorePointers(list);
+    // note: cannot use cxListAddArray() because we don't have a list of pointers
+    cx_for_n(i, 5) cxListAdd(list, &fivenums[i]);
+    verifyInsertViaIterator(list);
+}
+
+TEST_F(ArrayList, InsertViaIterator) {
+    int fivenums[] = {0, 1, 2, 3, 4, 5};
+    CxList *list = autofree(cxArrayListCreate(&testingAllocator, cx_cmp_int, sizeof(int), 4));
+    cxListAddArray(list, fivenums, 5);
+    verifyInsertViaIterator(list);
+}
+
+TEST_F(LinkedList, cxListReverse) {
+    verifyReverse(linkedListFromTestData());
+}
+
+TEST_F(PointerLinkedList, cxListReverse) {
+    verifyReverse(pointerLinkedListFromTestData());
+}
+
+TEST_F(ArrayList, cxListReverse) {
+    verifyReverse(arrayListFromTestData());
+}
+
+TEST_F(LinkedList, cxListCompare) {
+    auto left = linkedListFromTestData();
+    auto right = linkedListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(LinkedList, cxListCompareWithPtrList) {
+    auto left = linkedListFromTestData();
+    auto right = pointerLinkedListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(LinkedList, cxListCompareWithArrayList) {
+    auto left = linkedListFromTestData();
+    auto right = arrayListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(PointerLinkedList, cxListCompare) {
+    auto left = pointerLinkedListFromTestData();
+    auto right = pointerLinkedListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(PointerLinkedList, cxListCompareWithNormalList) {
+    auto left = pointerLinkedListFromTestData();
+    auto right = linkedListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(PointerLinkedList, cxListCompareWithArrayList) {
+    auto left = pointerLinkedListFromTestData();
+    auto right = arrayListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(ArrayList, cxListCompare) {
+    auto left = arrayListFromTestData();
+    auto right = arrayListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(ArrayList, cxListCompareWithPtrList) {
+    auto left = arrayListFromTestData();
+    auto right = pointerLinkedListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(ArrayList, cxListCompareWithNormalList) {
+    auto left = arrayListFromTestData();
+    auto right = linkedListFromTestData();
+    verifyCompare(left, right);
+}
+
+TEST_F(PointerLinkedList, NoDestructor) {
+    void *item = cxMalloc(&testingAllocator, sizeof(int));
+    auto list = cxLinkedListCreate(cxDefaultAllocator, cx_cmp_int, sizeof(int *));
+    cxListStorePointers(list);
+    cxListAdd(list, item);
+    ASSERT_FALSE(testingAllocator.verify());
+    cxListDestroy(list);
+    EXPECT_FALSE(testingAllocator.verify());
+    cxFree(&testingAllocator, item);
+    EXPECT_TRUE(testingAllocator.verify());
+}
+
+TEST_F(PointerLinkedList, SimpleDestructor) {
+    int item = 0;
+    auto list = cxLinkedListCreate(cxDefaultAllocator, cx_cmp_int, sizeof(int *));
+    cxListStorePointers(list);
+    list->content_destructor_type = CX_DESTRUCTOR_SIMPLE;
+    list->simple_destructor = [](void *elem) { *(int *) elem = 42; };
+    cxListAdd(list, &item);
+    cxListDestroy(list);
+    EXPECT_EQ(item, 42);
+}
+
+TEST_F(PointerLinkedList, AdvancedDestructor) {
+    void *item = cxMalloc(&testingAllocator, sizeof(int));
+    auto list = cxLinkedListCreate(cxDefaultAllocator, cx_cmp_int, sizeof(int *));
+    cxListStorePointers(list);
+    list->content_destructor_type = CX_DESTRUCTOR_ADVANCED;
+    list->advanced_destructor.data = &testingAllocator;
+    list->advanced_destructor.func = (cx_destructor_func2) cxFree;
+    cxListAdd(list, item);
+    ASSERT_FALSE(testingAllocator.verify());
+    cxListDestroy(list);
+    EXPECT_TRUE(testingAllocator.verify());
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_map.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,272 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/hash_map.h"
+#include "cx/utils.h"
+#include "util_allocator.h"
+
+#include <gtest/gtest.h>
+#include <unordered_map>
+#include <unordered_set>
+
+struct map_operation {
+    enum {
+        put, rm
+    } op;
+    char const *key;
+    char const *value;
+};
+
+auto generate_map_operations() -> std::vector<map_operation> {
+    return {
+            {map_operation::put, "key 1",          "test"},
+            {map_operation::put, "key 2",          "blub"},
+            {map_operation::put, "key 3",          "hallo"},
+            {map_operation::put, "key 2",          "foobar"},
+            {map_operation::put, "key 4",          "value 4"},
+            {map_operation::put, "key 5",          "value 5"},
+            {map_operation::put, "key 6",          "value 6"},
+            {map_operation::rm,  "key 4",          nullptr},
+            {map_operation::put, "key 7",          "value 7"},
+            {map_operation::put, "key 8",          "value 8"},
+            {map_operation::rm,  "does not exist", nullptr},
+            {map_operation::put, "key 9",          "value 9"},
+            {map_operation::put, "key 6",          "other value"},
+            {map_operation::put, "key 7",          "something else"},
+            {map_operation::rm,  "key 8",          nullptr},
+            {map_operation::rm,  "key 2",          nullptr},
+            {map_operation::put, "key 8",          "new value"},
+    };
+}
+
+static void verify_map_contents(
+        CxMap *map,
+        std::unordered_map<std::string, std::string> const &refmap
+) {
+    // verify key iterator
+    {
+        auto keyiter = cxMapIteratorKeys(map);
+        std::unordered_set<std::string> keys;
+        cx_foreach(CxHashKey*, elem, keyiter) {
+            keys.insert(std::string(elem->data.cstr, elem->len));
+        }
+        EXPECT_EQ(keyiter.index, map->size);
+        ASSERT_EQ(keys.size(), map->size);
+        for (auto &&k: keys) {
+            EXPECT_NE(refmap.find(k), refmap.end());
+        }
+    }
+
+    // verify value iterator
+    {
+        auto valiter = cxMapIteratorValues(map);
+        std::unordered_set<std::string> values; // we use that the values in our test data are unique strings
+        cx_foreach(char const*, elem, valiter) {
+            values.insert(std::string(elem));
+        }
+        EXPECT_EQ(valiter.index, map->size);
+        ASSERT_EQ(values.size(), map->size);
+        for (auto &&v: values) {
+            EXPECT_NE(std::find_if(refmap.begin(), refmap.end(),
+                                   [v](auto const &e) { return e.second == v; }), refmap.end());
+        }
+    }
+
+    // verify pair iterator
+    {
+        auto pairiter = cxMapIterator(map);
+        std::unordered_map<std::string, std::string> pairs;
+        cx_foreach(CxMapEntry*, entry, pairiter) {
+            pairs[std::string(entry->key->data.cstr, entry->key->len)] = std::string((char *) entry->value);
+        }
+        EXPECT_EQ(pairiter.index, map->size);
+        ASSERT_EQ(pairs.size(), refmap.size());
+        for (auto &&p: pairs) {
+            ASSERT_EQ(p.second, refmap.at(p.first));
+        }
+    }
+}
+
+TEST(CxHashMap, Create) {
+    CxTestingAllocator allocator;
+    auto map = cxHashMapCreate(&allocator, 0);
+    auto hmap = reinterpret_cast<struct cx_hash_map_s *>(map);
+    EXPECT_GT(hmap->bucket_count, 0);
+    cx_for_n(i, hmap->bucket_count) {
+        EXPECT_EQ(hmap->buckets[i], nullptr);
+    }
+    EXPECT_EQ(map->size, 0);
+    EXPECT_EQ(map->allocator, &allocator);
+
+    cxMapDestroy(map);
+    EXPECT_TRUE(allocator.verify());
+}
+
+TEST(CxHashMap, BasicOperations) {
+    // create the map
+    CxTestingAllocator allocator;
+    auto map = cxHashMapCreate(&allocator, 8);
+
+    // create a reference map
+    std::unordered_map<std::string, std::string> refmap;
+
+    // generate operations
+    auto ops = generate_map_operations();
+
+    // verify iterators for empty map
+    verify_map_contents(map, refmap);
+
+    // execute operations and verify results
+    for (auto &&op: ops) {
+        CxHashKey key = cx_hash_key_str(op.key);
+        key.hash = 0; // force the hash map to compute the hash
+        if (op.op == map_operation::put) {
+            // execute a put operation and verify that the exact value can be read back
+            refmap[std::string(op.key)] = std::string(op.value);
+            int result = cxMapPut(map, key, (void *) op.value);
+            EXPECT_EQ(result, 0);
+            auto added = cxMapGet(map, key);
+            EXPECT_EQ(memcmp(op.value, added, strlen(op.value)), 0);
+        } else {
+            // execute a remove and verify that the removed element was returned (or nullptr)
+            auto found = refmap.find(op.key);
+            auto removed = cxMapRemove(map, key);
+            if (found == refmap.end()) {
+                EXPECT_EQ(removed, nullptr);
+            } else {
+                EXPECT_EQ(std::string((char *) removed), found->second);
+                refmap.erase(found);
+            }
+        }
+        // compare the current map state with the reference map
+        verify_map_contents(map, refmap);
+    }
+
+    // destroy the map and verify the memory (de)allocations
+    cxMapDestroy(map);
+    EXPECT_TRUE(allocator.verify());
+}
+
+TEST(CxHashMap, RemoveViaIterator) {
+    CxTestingAllocator allocator;
+    auto map = cxHashMapCreate(&allocator, 4);
+
+    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
+    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
+    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
+    cxMapPut(map, cx_hash_key_str("key 4"), (void *) "val 4");
+    cxMapPut(map, cx_hash_key_str("key 5"), (void *) "val 5");
+    cxMapPut(map, cx_hash_key_str("key 6"), (void *) "val 6");
+
+    auto iter = cxMapMutIterator(map);
+    cx_foreach(CxMapEntry*, entry, iter) {
+        if (entry->key->data.cstr[4] % 2 == 1) cxIteratorFlagRemoval(iter);
+    }
+    EXPECT_EQ(map->size, 3);
+    EXPECT_EQ(iter.index, map->size);
+
+    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 1")), nullptr);
+    EXPECT_NE(cxMapGet(map, cx_hash_key_str("key 2")), nullptr);
+    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 3")), nullptr);
+    EXPECT_NE(cxMapGet(map, cx_hash_key_str("key 4")), nullptr);
+    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 5")), nullptr);
+    EXPECT_NE(cxMapGet(map, cx_hash_key_str("key 6")), nullptr);
+
+    cxMapDestroy(map);
+    EXPECT_TRUE(allocator.verify());
+}
+
+TEST(CxHashMap, RehashNotRequired) {
+    CxTestingAllocator allocator;
+    auto map = cxHashMapCreate(&allocator, 8);
+
+    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
+    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
+    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
+    cxMapPut(map, cx_hash_key_str("key 4"), (void *) "val 4");
+    cxMapPut(map, cx_hash_key_str("key 5"), (void *) "val 5");
+    cxMapPut(map, cx_hash_key_str("key 6"), (void *) "val 6");
+
+    // 6/8 does not exceed 0.75, therefore the function should not rehash
+    int result = cxMapRehash(map);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(reinterpret_cast<struct cx_hash_map_s *>(map)->bucket_count, 8);
+
+    cxMapDestroy(map);
+    EXPECT_TRUE(allocator.verify());
+}
+
+TEST(CxHashMap, Rehash) {
+    CxTestingAllocator allocator;
+    auto map = cxHashMapCreate(&allocator, 8);
+
+    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
+    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
+    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
+    cxMapPut(map, cx_hash_key_str("key 4"), (void *) "val 4");
+    cxMapPut(map, cx_hash_key_str("key 5"), (void *) "val 5");
+    cxMapPut(map, cx_hash_key_str("key 6"), (void *) "val 6");
+    cxMapPut(map, cx_hash_key_str("key 7"), (void *) "val 7");
+
+    int result = cxMapRehash(map);
+    EXPECT_EQ(result, 0);
+    EXPECT_EQ(reinterpret_cast<struct cx_hash_map_s *>(map)->bucket_count, 17);
+    EXPECT_EQ(map->size, 7);
+
+    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 1")), "val 1"), 0);
+    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 2")), "val 2"), 0);
+    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 3")), "val 3"), 0);
+    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 4")), "val 4"), 0);
+    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 5")), "val 5"), 0);
+    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 6")), "val 6"), 0);
+    EXPECT_EQ(strcmp((char *) cxMapGet(map, cx_hash_key_str("key 7")), "val 7"), 0);
+
+    cxMapDestroy(map);
+    EXPECT_TRUE(allocator.verify());
+}
+
+TEST(CxHashMap, Clear) {
+    CxTestingAllocator allocator;
+    auto map = cxHashMapCreate(&allocator, 0);
+    
+    cxMapPut(map, cx_hash_key_str("key 1"), (void *) "val 1");
+    cxMapPut(map, cx_hash_key_str("key 2"), (void *) "val 2");
+    cxMapPut(map, cx_hash_key_str("key 3"), (void *) "val 3");
+
+    EXPECT_EQ(map->size, 3);
+
+    cxMapClear(map);
+
+    EXPECT_EQ(map->size, 0);
+    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 1")), nullptr);
+    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 2")), nullptr);
+    EXPECT_EQ(cxMapGet(map, cx_hash_key_str("key 3")), nullptr);
+
+    cxMapDestroy(map);
+    EXPECT_TRUE(allocator.verify());
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_printf.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,248 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/printf.h"
+#include "cx/buffer.h"
+
+#include <gtest/gtest.h>
+#include "util_allocator.h"
+
+class PrintfFixture : public ::testing::Test {
+protected:
+    std::string buf;
+    CxTestingAllocator alloc;
+
+    void TearDown() override {
+        buf.clear();
+        ASSERT_TRUE(alloc.verify());
+    }
+
+    static size_t write_func(
+            void const *src,
+            size_t esize,
+            size_t ecount,
+            void *target
+    ) {
+        auto str = reinterpret_cast<char const *>(src);
+        auto buf = reinterpret_cast<std::string *>(target);
+        EXPECT_EQ(esize, 1);
+        EXPECT_EQ(strlen(str), ecount);
+        *buf = str;
+        return ecount;
+    }
+};
+
+
+TEST_F(PrintfFixture, BPrintf) {
+    CxBuffer buf;
+    cxBufferInit(&buf, nullptr, 64, &alloc, 0);
+
+    auto r = cx_bprintf(&buf, "This %s aged %u years in a %2XSK.", "Test", 10, 0xca);
+    EXPECT_EQ(r, 34);
+    EXPECT_EQ(buf.size, 34);
+    buf.space[r] = '\0';
+    EXPECT_STREQ(buf.space, "This Test aged 10 years in a CASK.");
+
+    cxBufferDestroy(&buf);
+}
+
+TEST_F(PrintfFixture, FPrintf) {
+    auto h = "Hello";
+    size_t r;
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "teststring");
+    EXPECT_EQ(r, 10);
+    EXPECT_EQ(buf, "teststring");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%10s]", h);
+    EXPECT_EQ(r, 12);
+    EXPECT_EQ(buf, "[     Hello]");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%-10s]", h);
+    EXPECT_EQ(r, 12);
+    EXPECT_EQ(buf, "[Hello     ]");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%*s]", 10, h);
+    EXPECT_EQ(r, 12);
+    EXPECT_EQ(buf, "[     Hello]");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%-10.*s]", 4, h);
+    EXPECT_EQ(r, 12);
+    EXPECT_EQ(buf, "[Hell      ]");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "[%-*.*s]", 10, 4, h);
+    EXPECT_EQ(r, 12);
+    EXPECT_EQ(buf, "[Hell      ]");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "%c", 'A');
+    EXPECT_EQ(r, 1);
+    EXPECT_EQ(buf, "A");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "%i %d %.6i %i %.0i %+i %i", 1, 2, 3, 0, 0, 4, -4);
+    EXPECT_EQ(r, 19);
+    EXPECT_EQ(buf, "1 2 000003 0  +4 -4");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "%x %x %X %#x", 5, 10, 10, 6);
+    EXPECT_EQ(r, 9);
+    EXPECT_EQ(buf, "5 a A 0x6");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "%o %#o %#o", 10, 10, 4);
+    EXPECT_EQ(r, 9);
+    EXPECT_EQ(buf, "12 012 04");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "%05.2f %.2f %5.2f", 1.5, 1.5, 1.5);
+    EXPECT_EQ(r, 16);
+    EXPECT_EQ(buf, "01.50 1.50  1.50");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "'%*c'", 5, 'x');
+    EXPECT_EQ(r, 7);
+    EXPECT_EQ(buf, "'    x'");
+
+    r = cx_fprintf(&buf, PrintfFixture::write_func, "'%*c'", -5, 'x');
+    EXPECT_EQ(r, 7);
+    EXPECT_EQ(buf, "'x    '");
+}
+
+TEST_F(PrintfFixture, BPrintfLargeString) {
+    CxBuffer buf;
+    cxBufferInit(&buf, nullptr, 64, &alloc, CX_BUFFER_AUTO_EXTEND);
+
+    auto aaa = std::string(512, 'a');
+    auto bbb = std::string(512, 'b');
+
+    auto r = cx_bprintf(&buf, "After %s comes %s.", aaa.data(), bbb.data());
+    EXPECT_EQ(r, 1038);
+    EXPECT_EQ(buf.size, 1038);
+    cxBufferPut(&buf, 0);
+    EXPECT_EQ(buf.space, std::string("After ") + aaa + " comes " + bbb + ".");
+
+    cxBufferDestroy(&buf);
+}
+
+TEST_F(PrintfFixture, BPrintfNoCap) {
+    CxBuffer buf;
+    char space[20];
+    memset(space, 'a', 20);
+    cxBufferInit(&buf, space, 16, &alloc, 0);
+
+    auto r = cx_bprintf(&buf, "Hello %s with more than %d chars.", "string", 16);
+    EXPECT_EQ(r, 16);
+    EXPECT_EQ(buf.size, 16);
+    EXPECT_EQ(0, memcmp(space, "Hello string witaaaa", 20));
+
+    cxBufferDestroy(&buf);
+}
+
+TEST_F(PrintfFixture, SPrintf) {
+    auto h = "Hello";
+
+    std::vector<char *> fl;
+    cxmutstr r;
+
+    r = cx_asprintf_a(&alloc, "teststring");
+    EXPECT_EQ(r.length, 10);
+    EXPECT_STREQ(r.ptr, "teststring");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "[%10s]", h);
+    EXPECT_EQ(r.length, 12);
+    EXPECT_STREQ(r.ptr, "[     Hello]");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "[%-10s]", h);
+    EXPECT_EQ(r.length, 12);
+    EXPECT_STREQ(r.ptr, "[Hello     ]");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "[%*s]", 10, h);
+    EXPECT_EQ(r.length, 12);
+    EXPECT_STREQ(r.ptr, "[     Hello]");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "[%-10.*s]", 4, h);
+    EXPECT_EQ(r.length, 12);
+    EXPECT_STREQ(r.ptr, "[Hell      ]");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "[%-*.*s]", 10, 4, h);
+    EXPECT_EQ(r.length, 12);
+    EXPECT_STREQ(r.ptr, "[Hell      ]");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "%c", 'A');
+    EXPECT_EQ(r.length, 1);
+    EXPECT_STREQ(r.ptr, "A");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "%i %d %.6i %i %.0i %+i %i", 1, 2, 3, 0, 0, 4, -4);
+    EXPECT_EQ(r.length, 19);
+    EXPECT_STREQ(r.ptr, "1 2 000003 0  +4 -4");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "%x %x %X %#x", 5, 10, 10, 6);
+    EXPECT_EQ(r.length, 9);
+    EXPECT_STREQ(r.ptr, "5 a A 0x6");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "%o %#o %#o", 10, 10, 4);
+    EXPECT_EQ(r.length, 9);
+    EXPECT_STREQ(r.ptr, "12 012 04");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "%05.2f %.2f %5.2f", 1.5, 1.5, 1.5);
+    EXPECT_EQ(r.length, 16);
+    EXPECT_STREQ(r.ptr, "01.50 1.50  1.50");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "'%*c'", 5, 'x');
+    EXPECT_EQ(r.length, 7);
+    EXPECT_STREQ(r.ptr, "'    x'");
+    fl.push_back(r.ptr);
+
+    r = cx_asprintf_a(&alloc, "'%*c'", -5, 'x');
+    EXPECT_EQ(r.length, 7);
+    EXPECT_STREQ(r.ptr, "'x    '");
+    fl.push_back(r.ptr);
+
+    for (auto c: fl) {
+        auto s = cx_mutstrn(c, 0);
+        cx_strfree_a(&alloc, &s);
+    }
+}
+
+TEST_F(PrintfFixture, SPrintfLargeString) {
+    auto aaa = std::string(512, 'a');
+    auto bbb = std::string(512, 'b');
+
+    auto r = cx_asprintf_a(&alloc, "After %s comes %s.", aaa.data(), bbb.data());
+    EXPECT_EQ(r.length, 1038);
+    EXPECT_EQ(r.ptr, std::string("After ") + aaa + " comes " + bbb + ".");
+    EXPECT_EQ(r.ptr[1038], '\0');
+
+    cx_strfree_a(&alloc, &r);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_string.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,865 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/string.h"
+#include "util_allocator.h"
+
+#include <gtest/gtest.h>
+
+#define EXPECT_ZERO_TERMINATED(str) EXPECT_EQ((str).ptr[(str).length], '\0')
+
+TEST(String, construct) {
+    cxstring s1 = cx_str("1234");
+    cxstring s2 = cx_strn("abcd", 2);
+    cxmutstr s3 = cx_mutstr((char *) "1234");
+    cxmutstr s4 = cx_mutstrn((char *) "abcd", 2);
+
+    EXPECT_EQ(s1.length, 4);
+    EXPECT_EQ(s2.length, 2);
+    EXPECT_EQ(s3.length, 4);
+    EXPECT_EQ(s4.length, 2);
+}
+
+TEST(String, strfree) {
+    CxTestingAllocator alloc;
+    auto test = (char *) cxMalloc(&alloc, 16);
+    cxmutstr str = cx_mutstrn(test, 16);
+    ASSERT_EQ(str.ptr, test);
+    EXPECT_EQ(str.length, 16);
+    cx_strfree_a(&alloc, &str);
+    EXPECT_EQ(str.ptr, nullptr);
+    EXPECT_EQ(str.length, 0);
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(String, strdup) {
+    cxstring str = CX_STR("test");
+    cxmutstr dup = cx_strdup(str);
+    ASSERT_EQ(dup.length, str.length);
+    EXPECT_STREQ(dup.ptr, str.ptr);
+    EXPECT_ZERO_TERMINATED(dup);
+    cx_strfree(&dup);
+
+    str.length = 2;
+    dup = cx_strdup(str);
+    ASSERT_EQ(dup.length, str.length);
+    EXPECT_STREQ(dup.ptr, "te");
+    EXPECT_ZERO_TERMINATED(dup);
+    cx_strfree(&dup);
+}
+
+TEST(String, strlen) {
+    cxstring s1 = CX_STR("1234");
+    cxstring s2 = CX_STR(".:.:.");
+    cxstring s3 = CX_STR("X");
+
+    size_t len0 = cx_strlen(0);
+    size_t len1 = cx_strlen(1, s1);
+    size_t len2 = cx_strlen(2, s1, s2);
+    size_t len3 = cx_strlen(3, s1, s2, s3);
+
+    EXPECT_EQ(len0, 0);
+    EXPECT_EQ(len1, 4);
+    EXPECT_EQ(len2, 9);
+    EXPECT_EQ(len3, 10);
+}
+
+TEST(String, strsubs) {
+    cxstring str = CX_STR("A test string");
+
+    cxstring sub = cx_strsubs(str, 0);
+    EXPECT_EQ(cx_strcmp(sub, str), 0);
+
+    sub = cx_strsubs(str, 2);
+    EXPECT_EQ(cx_strcmp(sub, cx_str("test string")), 0);
+
+    sub = cx_strsubs(str, 7);
+    EXPECT_EQ(cx_strcmp(sub, cx_str("string")), 0);
+
+    sub = cx_strsubs(str, 15);
+    EXPECT_EQ(cx_strcmp(sub, cx_str("")), 0);
+
+    sub = cx_strsubsl(str, 2, 4);
+    EXPECT_EQ(cx_strcmp(sub, cx_str("test")), 0);
+
+    sub = cx_strsubsl(str, 7, 3);
+    EXPECT_EQ(cx_strcmp(sub, cx_str("str")), 0);
+
+    sub = cx_strsubsl(str, 7, 20);
+    EXPECT_EQ(cx_strcmp(sub, cx_str("string")), 0);
+
+    // just for coverage, call the _m variant
+    auto m = cx_strsubs_m(cx_mutstrn(nullptr, 0), 0);
+    EXPECT_EQ(cx_strcmp(cx_strcast(m), cx_str("")), 0);
+}
+
+TEST(String, strchr) {
+    cxstring str = CX_STR("I will find you - and I will kill you");
+
+    cxstring notfound = cx_strchr(str, 'x');
+    EXPECT_EQ(notfound.length, 0);
+
+    cxstring result = cx_strchr(str, 'w');
+    EXPECT_EQ(result.length, 35);
+    EXPECT_STREQ(result.ptr, "will find you - and I will kill you");
+
+    // just for coverage, call the _m variant
+    auto m = cx_strchr_m(cx_mutstrn(nullptr, 0), 'a');
+    EXPECT_EQ(cx_strcmp(cx_strcast(m), cx_str("")), 0);
+}
+
+TEST(String, strrchr) {
+    cxstring str = CX_STR("I will find you - and I will kill you");
+
+    cxstring notfound = cx_strrchr(str, 'x');
+    EXPECT_EQ(notfound.length, 0);
+
+    cxstring result = cx_strrchr(str, 'w');
+    EXPECT_EQ(result.length, 13);
+    EXPECT_STREQ(result.ptr, "will kill you");
+
+    // just for coverage, call the _m variant
+    auto m = cx_strrchr_m(cx_mutstrn(nullptr, 0), 'a');
+    EXPECT_EQ(cx_strcmp(cx_strcast(m), cx_str("")), 0);
+}
+
+TEST(String, strstr) {
+    cxstring str = CX_STR("find the match in this string");
+    cxstring longstr = CX_STR(
+            "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
+            "mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx"
+            "yzabcdeababababnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij"
+            "klmnopqrstuvwxyzaababababababababrstuvwxyzabcdefghijklmnopqrstuv"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "wxyz1234567890");
+    cxstring longstrpattern = CX_STR(
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+    );
+    cxstring longstrresult = CX_STR(
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "abababababababababababababababababababababababababababababababab"
+            "wxyz1234567890"
+    );
+
+    cxstring notfound = cx_strstr(str, cx_str("no match"));
+    EXPECT_EQ(notfound.length, 0);
+
+    cxstring result = cx_strstr(str, cx_str("match"));
+    EXPECT_EQ(result.length, 20);
+    EXPECT_STREQ(result.ptr, "match in this string");
+
+    result = cx_strstr(str, cx_str(""));
+    EXPECT_EQ(result.length, str.length);
+    EXPECT_STREQ(result.ptr, str.ptr);
+
+    result = cx_strstr(longstr, longstrpattern);
+    EXPECT_EQ(result.length, longstrresult.length);
+    EXPECT_STREQ(result.ptr, longstrresult.ptr);
+
+    // just for coverage, call the _m variant
+    auto mstr = cx_strdup(longstr);
+    auto m = cx_strstr_m(mstr, longstrpattern);
+    EXPECT_EQ(m.length, longstrresult.length);
+    EXPECT_STREQ(m.ptr, longstrresult.ptr);
+    cx_strfree(&mstr);
+}
+
+TEST(String, strcmp) {
+    cxstring str = CX_STR("compare this");
+
+    EXPECT_EQ(cx_strcmp(cx_str(""), cx_str("")), 0);
+    EXPECT_GT(cx_strcmp(str, cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(str, cx_str("compare this")), 0);
+    EXPECT_NE(cx_strcmp(str, cx_str("Compare This")), 0);
+    EXPECT_LT(cx_strcmp(str, cx_str("compare tool")), 0);
+    EXPECT_GT(cx_strcmp(str, cx_str("compare shit")), 0);
+    EXPECT_LT(cx_strcmp(str, cx_str("compare this not")), 0);
+    EXPECT_GT(cx_strcmp(str, cx_str("compare")), 0);
+}
+
+TEST(String, strcasecmp) {
+    cxstring str = CX_STR("compare this");
+
+    EXPECT_EQ(cx_strcasecmp(cx_str(""), cx_str("")), 0);
+    EXPECT_GT(cx_strcasecmp(str, cx_str("")), 0);
+    EXPECT_EQ(cx_strcasecmp(str, cx_str("compare this")), 0);
+    EXPECT_EQ(cx_strcasecmp(str, cx_str("Compare This")), 0);
+    EXPECT_LT(cx_strcasecmp(str, cx_str("compare tool")), 0);
+    EXPECT_GT(cx_strcasecmp(str, cx_str("compare shit")), 0);
+    EXPECT_LT(cx_strcasecmp(str, cx_str("compare this not")), 0);
+    EXPECT_GT(cx_strcasecmp(str, cx_str("compare")), 0);
+}
+
+TEST(String, strcat) {
+    cxstring s1 = CX_STR("12");
+    cxstring s2 = CX_STR("34");
+    cxstring s3 = CX_STR("56");
+    cxstring sn = {nullptr, 0};
+
+    CxTestingAllocator alloc;
+
+    cxmutstr t1 = cx_strcat_a(&alloc, 2, s1, s2);
+    EXPECT_EQ(cx_strcmp(cx_strcast(t1), cx_str("1234")), 0);
+    EXPECT_ZERO_TERMINATED(t1);
+    cx_strfree_a(&alloc, &t1);
+
+    cxmutstr t2 = cx_strcat_a(&alloc, 3, s1, s2, s3);
+    EXPECT_EQ(cx_strcmp(cx_strcast(t2), cx_str("123456")), 0);
+    EXPECT_ZERO_TERMINATED(t2);
+    cx_strfree_a(&alloc, &t2);
+
+    cxmutstr t3 = cx_strcat_a(&alloc, 6, s1, sn, s2, sn, s3, sn);
+    EXPECT_EQ(cx_strcmp(cx_strcast(t3), cx_str("123456")), 0);
+    EXPECT_ZERO_TERMINATED(t3);
+    cx_strfree_a(&alloc, &t3);
+
+    cxmutstr t4 = cx_strcat_a(&alloc, 2, sn, sn);
+    EXPECT_EQ(cx_strcmp(cx_strcast(t4), cx_str("")), 0);
+    EXPECT_ZERO_TERMINATED(t4);
+    cx_strfree_a(&alloc, &t4);
+
+    EXPECT_TRUE(alloc.verify());
+
+    // use the macro
+    cxmutstr t5 = cx_strcat(3, s3, s1, s2);
+    EXPECT_EQ(cx_strcmp(cx_strcast(t5), cx_str("561234")), 0);
+    EXPECT_ZERO_TERMINATED(t5);
+    cx_strfree(&t5);
+}
+
+TEST(String, strsplit) {
+
+    cxstring test = cx_str("this,is,a,csv,string");
+    size_t capa = 8;
+    cxstring list[8];
+    size_t n;
+
+    // special case: empty string
+    n = cx_strsplit(test, cx_str(""), capa, list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+
+    // no delimiter occurrence
+    n = cx_strsplit(test, cx_str("z"), capa, list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+
+    // partially matching delimiter
+    n = cx_strsplit(test, cx_str("is,not"), capa, list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+
+    // matching single-char delimiter
+    n = cx_strsplit(test, cx_str(","), capa, list);
+    ASSERT_EQ(n, 5);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("a")), 0);
+    EXPECT_EQ(cx_strcmp(list[3], cx_str("csv")), 0);
+    EXPECT_EQ(cx_strcmp(list[4], cx_str("string")), 0);
+
+    // matching multi-char delimiter
+    n = cx_strsplit(test, cx_str("is"), capa, list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str(",")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str(",a,csv,string")), 0);
+
+    // bounded list using single-char delimiter
+    n = cx_strsplit(test, cx_str(","), 3, list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
+
+    // bounded list using multi-char delimiter
+    n = cx_strsplit(test, cx_str("is"), 2, list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
+
+    // start with delimiter
+    n = cx_strsplit(test, cx_str("this"), capa, list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
+
+    // end with delimiter
+    n = cx_strsplit(test, cx_str("string"), capa, list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("this,is,a,csv,")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
+
+
+    // end with delimiter exceed bound
+    n = cx_strsplit(cx_str("a,b,c,"), cx_str(","), 3, list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("a")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("b")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("c,")), 0);
+
+    // exact match
+    n = cx_strsplit(test, cx_str("this,is,a,csv,string"), capa, list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
+
+    // string to be split is only substring
+    n = cx_strsplit(test, cx_str("this,is,a,csv,string,with,extension"), capa, list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+
+    // subsequent encounter of delimiter (the string between is empty)
+    n = cx_strsplit(test, cx_str("is,"), capa, list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
+
+    // call the _m variant just for coverage
+    auto mtest = cx_strdup(test);
+    cxmutstr mlist[4];
+    n = cx_strsplit_m(mtest, cx_str("is,"), 4, mlist);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[0]), cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[1]), cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[2]), cx_str("a,csv,string")), 0);
+    cx_strfree(&mtest);
+}
+
+TEST(String, strsplit_a) {
+    CxTestingAllocator alloc;
+
+    cxstring test = cx_str("this,is,a,csv,string");
+    size_t capa = 8;
+    cxstring *list;
+    size_t n;
+
+    // special case: empty string
+    n = cx_strsplit_a(&alloc, test, cx_str(""), capa, &list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+    cxFree(&alloc, list);
+
+    // no delimiter occurrence
+    n = cx_strsplit_a(&alloc, test, cx_str("z"), capa, &list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+    cxFree(&alloc, list);
+
+    // partially matching delimiter
+    n = cx_strsplit_a(&alloc, test, cx_str("is,not"), capa, &list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+    cxFree(&alloc, list);
+
+    // matching single-char delimiter
+    n = cx_strsplit_a(&alloc, test, cx_str(","), capa, &list);
+    ASSERT_EQ(n, 5);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("a")), 0);
+    EXPECT_EQ(cx_strcmp(list[3], cx_str("csv")), 0);
+    EXPECT_EQ(cx_strcmp(list[4], cx_str("string")), 0);
+    cxFree(&alloc, list);
+
+    // matching multi-char delimiter
+    n = cx_strsplit_a(&alloc, test, cx_str("is"), capa, &list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str(",")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str(",a,csv,string")), 0);
+    cxFree(&alloc, list);
+
+    // bounded list using single-char delimiter
+    n = cx_strsplit_a(&alloc, test, cx_str(","), 3, &list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("this")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("is")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
+    cxFree(&alloc, list);
+
+    // bounded list using multi-char delimiter
+    n = cx_strsplit_a(&alloc, test, cx_str("is"), 2, &list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
+    cxFree(&alloc, list);
+
+    // start with delimiter
+    n = cx_strsplit_a(&alloc, test, cx_str("this"), capa, &list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str(",is,a,csv,string")), 0);
+    cxFree(&alloc, list);
+
+    // end with delimiter
+    n = cx_strsplit_a(&alloc, test, cx_str("string"), capa, &list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("this,is,a,csv,")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
+    cxFree(&alloc, list);
+
+    // end with delimiter exceed bound
+    n = cx_strsplit_a(&alloc, cx_str("a,b,c,"), cx_str(","), 3, &list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("a")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("b")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("c,")), 0);
+    cxFree(&alloc, list);
+
+    // exact match
+    n = cx_strsplit_a(&alloc, test, cx_str("this,is,a,csv,string"), capa, &list);
+    ASSERT_EQ(n, 2);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
+    cxFree(&alloc, list);
+
+    // string to be split is only substring
+    n = cx_strsplit_a(&alloc, test, cx_str("this,is,a,csv,string,with,extension"), capa, &list);
+    ASSERT_EQ(n, 1);
+    EXPECT_EQ(cx_strcmp(list[0], test), 0);
+    cxFree(&alloc, list);
+
+    // subsequent encounter of delimiter (the string between is empty)
+    n = cx_strsplit_a(&alloc, test, cx_str("is,"), capa, &list);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(list[0], cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(list[1], cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(list[2], cx_str("a,csv,string")), 0);
+    cxFree(&alloc, list);
+
+    // call the _m variant just for coverage
+    auto mtest = cx_strdup(test);
+    cxmutstr *mlist;
+    n = cx_strsplit_ma(&alloc, mtest, cx_str("is,"), 4, &mlist);
+    ASSERT_EQ(n, 3);
+    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[0]), cx_str("th")), 0);
+    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[1]), cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(cx_strcast(mlist[2]), cx_str("a,csv,string")), 0);
+    cxFree(&alloc, mlist);
+    cx_strfree(&mtest);
+
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(String, strtrim) {
+    cxstring t1 = cx_strtrim(cx_str("  ein test  \t "));
+    cxstring t2 = cx_strtrim(cx_str("abc"));
+    cxstring t3 = cx_strtrim(cx_str(" 123"));
+    cxstring t4 = cx_strtrim(cx_str("xyz "));
+    cxstring t5 = cx_strtrim(cx_str("   "));
+    cxstring empty = cx_strtrim(cx_str(""));
+
+    EXPECT_EQ(cx_strcmp(t1, cx_str("ein test")), 0);
+    EXPECT_EQ(cx_strcmp(t2, cx_str("abc")), 0);
+    EXPECT_EQ(cx_strcmp(t3, cx_str("123")), 0);
+    EXPECT_EQ(cx_strcmp(t4, cx_str("xyz")), 0);
+    EXPECT_EQ(cx_strcmp(t5, cx_str("")), 0);
+    EXPECT_EQ(cx_strcmp(empty, cx_str("")), 0);
+
+    // call the _m variant just for coverage
+    cxmutstr m1 = cx_strtrim_m(cx_mutstr((char *) "  ein test  \t "));
+    EXPECT_EQ(cx_strcmp(cx_strcast(m1), cx_str("ein test")), 0);
+}
+
+TEST(String, strprefix) {
+    cxstring str = CX_STR("test my prefix and my suffix");
+    cxstring empty = CX_STR("");
+    EXPECT_FALSE(cx_strprefix(empty, cx_str("pref")));
+    EXPECT_TRUE(cx_strprefix(str, empty));
+    EXPECT_TRUE(cx_strprefix(empty, empty));
+    EXPECT_TRUE(cx_strprefix(str, cx_str("test ")));
+    EXPECT_FALSE(cx_strprefix(str, cx_str("8-) fsck ")));
+}
+
+TEST(String, strsuffix) {
+    cxstring str = CX_STR("test my prefix and my suffix");
+    cxstring empty = CX_STR("");
+    EXPECT_FALSE(cx_strsuffix(empty, cx_str("suf")));
+    EXPECT_TRUE(cx_strsuffix(str, empty));
+    EXPECT_TRUE(cx_strsuffix(empty, empty));
+    EXPECT_TRUE(cx_strsuffix(str, cx_str("fix")));
+    EXPECT_FALSE(cx_strsuffix(str, cx_str("fox")));
+}
+
+TEST(String, strcaseprefix) {
+    cxstring str = CX_STR("test my prefix and my suffix");
+    cxstring empty = CX_STR("");
+    EXPECT_FALSE(cx_strcaseprefix(empty, cx_str("pREf")));
+    EXPECT_TRUE(cx_strcaseprefix(str, empty));
+    EXPECT_TRUE(cx_strcaseprefix(empty, empty));
+    EXPECT_TRUE(cx_strcaseprefix(str, cx_str("TEST ")));
+    EXPECT_FALSE(cx_strcaseprefix(str, cx_str("8-) fsck ")));
+}
+
+TEST(String, strcasesuffix) {
+    cxstring str = CX_STR("test my prefix and my suffix");
+    cxstring empty = CX_STR("");
+    EXPECT_FALSE(cx_strcasesuffix(empty, cx_str("sUf")));
+    EXPECT_TRUE(cx_strcasesuffix(str, empty));
+    EXPECT_TRUE(cx_strcasesuffix(empty, empty));
+    EXPECT_TRUE(cx_strcasesuffix(str, cx_str("FIX")));
+    EXPECT_FALSE(cx_strcasesuffix(str, cx_str("fox")));
+}
+
+TEST(String, strreplace) {
+    CxTestingAllocator alloc;
+    cxstring str = CX_STR("test ababab string aba");
+    cxstring longstr = CX_STR(
+            "xyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacd");
+    cxstring notrail = CX_STR("test abab");
+    cxstring empty = CX_STR("");
+    cxstring astr = CX_STR("aaaaaaaaaa");
+    cxstring csstr = CX_STR("test AB ab TEST xyz");
+
+    cxmutstr repl = cx_strreplace(str, cx_str("abab"), cx_str("muchlonger"));
+    auto expected = "test muchlongerab string aba";
+
+    cxmutstr repln = cx_strreplacen(str, cx_str("ab"), cx_str("c"), 2);
+    auto expectedn = "test ccab string aba";
+
+    cxmutstr longrepl = cx_strreplace(longstr, cx_str("a"), cx_str("z"));
+    auto longexpect = "xyzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzcd";
+
+    cxmutstr replnotrail = cx_strreplace(notrail, cx_str("ab"), cx_str("z"));
+    auto notrailexpect = "test zz";
+
+    cxmutstr repleq = cx_strreplace(str, str, cx_str("hello"));
+    auto eqexpect = "hello";
+
+    cxmutstr replempty1 = cx_strreplace(empty, cx_str("ab"), cx_str("c")); // expect: empty
+    cxmutstr replempty2 = cx_strreplace(str, cx_str("abab"), empty);
+    auto emptyexpect2 = "test ab string aba";
+
+    cxmutstr replpre = cx_strreplace(str, cx_str("test "), cx_str("TEST "));
+    auto preexpected = "TEST ababab string aba";
+
+    cxmutstr replan1 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 1);
+    auto an1expected = "xaaaaaaaaa";
+
+    cxmutstr replan4 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 4);
+    auto an4expected = "xxxxaaaaaa";
+
+    cxmutstr replan9 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 9);
+    auto an9expected = "xxxxxxxxxa";
+
+    cxmutstr replan10 = cx_strreplacen(astr, cx_str("a"), cx_str("x"), 10);
+    auto an10expected = "xxxxxxxxxx";
+
+    cxmutstr repl1_a = cx_strreplace_a(&alloc, csstr, cx_str("AB"), cx_str("*"));
+    auto expeced1_a = "test * ab TEST xyz";
+
+    cxmutstr repl2_a = cx_strreplace_a(&alloc, csstr, cx_str("test"), cx_str("TEST"));
+    auto expected2_a = "TEST AB ab TEST xyz";
+
+
+    EXPECT_NE(repl.ptr, str.ptr);
+    EXPECT_ZERO_TERMINATED(repl);
+    EXPECT_STREQ(repl.ptr, expected);
+    EXPECT_ZERO_TERMINATED(repln);
+    EXPECT_STREQ(repln.ptr, expectedn);
+    EXPECT_ZERO_TERMINATED(longrepl);
+    EXPECT_STREQ(longrepl.ptr, longexpect);
+    EXPECT_ZERO_TERMINATED(replnotrail);
+    EXPECT_STREQ(replnotrail.ptr, notrailexpect);
+    EXPECT_ZERO_TERMINATED(repleq);
+    EXPECT_STREQ(repleq.ptr, eqexpect);
+    EXPECT_ZERO_TERMINATED(replempty1);
+    EXPECT_STREQ(replempty1.ptr, "");
+    EXPECT_ZERO_TERMINATED(replempty2);
+    EXPECT_STREQ(replempty2.ptr, emptyexpect2);
+    EXPECT_ZERO_TERMINATED(replpre);
+    EXPECT_STREQ(replpre.ptr, preexpected);
+    EXPECT_ZERO_TERMINATED(replan1);
+    EXPECT_STREQ(replan1.ptr, an1expected);
+    EXPECT_ZERO_TERMINATED(replan4);
+    EXPECT_STREQ(replan4.ptr, an4expected);
+    EXPECT_ZERO_TERMINATED(replan9);
+    EXPECT_STREQ(replan9.ptr, an9expected);
+    EXPECT_ZERO_TERMINATED(replan10);
+    EXPECT_STREQ(replan10.ptr, an10expected);
+    EXPECT_ZERO_TERMINATED(repl1_a);
+    EXPECT_STREQ(repl1_a.ptr, expeced1_a);
+    EXPECT_ZERO_TERMINATED(repl2_a);
+    EXPECT_STREQ(repl2_a.ptr, expected2_a);
+
+    cx_strfree(&repl);
+    cx_strfree(&repln);
+    cx_strfree(&longrepl);
+    cx_strfree(&replnotrail);
+    cx_strfree(&repleq);
+    cx_strfree(&replempty1);
+    cx_strfree(&replempty2);
+    cx_strfree(&replpre);
+    cx_strfree(&replan1);
+    cx_strfree(&replan4);
+    cx_strfree(&replan9);
+    cx_strfree(&replan10);
+
+    cx_strfree_a(&alloc, &repl1_a);
+    cx_strfree_a(&alloc, &repl2_a);
+    EXPECT_TRUE(alloc.verify());
+}
+
+TEST(String, strupper) {
+    cxmutstr str = cx_strdup(cx_str("thIs 1s @ Te$t"));
+    cx_strupper(str);
+    EXPECT_STREQ(str.ptr, "THIS 1S @ TE$T");
+    cx_strfree(&str);
+}
+
+TEST(String, strlower) {
+    cxmutstr str = cx_strdup(cx_str("thIs 1s @ Te$t"));
+    cx_strlower(str);
+    EXPECT_STREQ(str.ptr, "this 1s @ te$t");
+    cx_strfree(&str);
+}
+
+TEST(String, strtok) {
+    cxstring str = cx_str("a,comma,separated,string");
+    cxstring delim = cx_str(",");
+    CxStrtokCtx ctx = cx_strtok(str, delim, 3);
+    EXPECT_EQ(ctx.str.ptr, str.ptr);
+    EXPECT_EQ(ctx.str.length, str.length);
+    EXPECT_EQ(ctx.delim.ptr, delim.ptr);
+    EXPECT_EQ(ctx.delim.length, delim.length);
+    EXPECT_EQ(ctx.limit, 3);
+    EXPECT_EQ(ctx.found, 0);
+    EXPECT_EQ(ctx.pos, 0);
+    EXPECT_EQ(ctx.next_pos, 0);
+    EXPECT_EQ(ctx.delim_more, nullptr);
+    EXPECT_EQ(ctx.delim_more_count, 0);
+}
+
+TEST(String, strtok_m) {
+    cxmutstr str = cx_strdup(cx_str("a,comma,separated,string"));
+    cxstring delim = cx_str(",");
+    CxStrtokCtx ctx = cx_strtok_m(str, delim, 3);
+    EXPECT_EQ(ctx.str.ptr, str.ptr);
+    EXPECT_EQ(ctx.str.length, str.length);
+    EXPECT_EQ(ctx.delim.ptr, delim.ptr);
+    EXPECT_EQ(ctx.delim.length, delim.length);
+    EXPECT_EQ(ctx.limit, 3);
+    EXPECT_EQ(ctx.found, 0);
+    EXPECT_EQ(ctx.pos, 0);
+    EXPECT_EQ(ctx.next_pos, 0);
+    EXPECT_EQ(ctx.delim_more, nullptr);
+    EXPECT_EQ(ctx.delim_more_count, 0);
+    cx_strfree(&str);
+}
+
+TEST(String, strtok_delim) {
+    cxstring str = cx_str("an,arbitrarily|separated;string");
+    cxstring delim = cx_str(",");
+    cxstring delim_more[2] = {CX_STR("|"), CX_STR(";")};
+    CxStrtokCtx ctx = cx_strtok(str, delim, 3);
+    cx_strtok_delim(&ctx, delim_more, 2);
+    EXPECT_EQ(ctx.str.ptr, str.ptr);
+    EXPECT_EQ(ctx.str.length, str.length);
+    EXPECT_EQ(ctx.delim.ptr, delim.ptr);
+    EXPECT_EQ(ctx.delim.length, delim.length);
+    EXPECT_EQ(ctx.limit, 3);
+    EXPECT_EQ(ctx.found, 0);
+    EXPECT_EQ(ctx.pos, 0);
+    EXPECT_EQ(ctx.next_pos, 0);
+    EXPECT_EQ(ctx.delim_more, delim_more);
+    EXPECT_EQ(ctx.delim_more_count, 2);
+}
+
+TEST(String, strtok_next_easy) {
+    cxstring str = cx_str("a,comma,separated,string");
+    cxstring delim = cx_str(",");
+    CxStrtokCtx ctx = cx_strtok(str, delim, 3);
+    bool ret;
+    cxstring tok;
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("a")), 0);
+    EXPECT_EQ(ctx.pos, 0);
+    EXPECT_EQ(ctx.next_pos, 2);
+    EXPECT_EQ(ctx.delim_pos, 1);
+    EXPECT_EQ(ctx.found, 1);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("comma")), 0);
+    EXPECT_EQ(ctx.pos, 2);
+    EXPECT_EQ(ctx.next_pos, 8);
+    EXPECT_EQ(ctx.delim_pos, 7);
+    EXPECT_EQ(ctx.found, 2);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("separated")), 0);
+    EXPECT_EQ(ctx.pos, 8);
+    EXPECT_EQ(ctx.next_pos, 18);
+    EXPECT_EQ(ctx.delim_pos, 17);
+    EXPECT_EQ(ctx.found, 3);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_FALSE(ret);
+    EXPECT_EQ(ctx.pos, 8);
+    EXPECT_EQ(ctx.next_pos, 18);
+    EXPECT_EQ(ctx.delim_pos, 17);
+    EXPECT_EQ(ctx.found, 3);
+}
+
+TEST(String, strtok_next_unlimited) {
+    cxstring str = cx_str("some;-;otherwise;-;separated;-;string;-;");
+    cxstring delim = cx_str(";-;");
+    CxStrtokCtx ctx = cx_strtok(str, delim, SIZE_MAX);
+    bool ret;
+    cxstring tok;
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("some")), 0);
+    EXPECT_EQ(ctx.pos, 0);
+    EXPECT_EQ(ctx.next_pos, 7);
+    EXPECT_EQ(ctx.delim_pos, 4);
+    EXPECT_EQ(ctx.found, 1);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("otherwise")), 0);
+    EXPECT_EQ(ctx.pos, 7);
+    EXPECT_EQ(ctx.next_pos, 19);
+    EXPECT_EQ(ctx.delim_pos, 16);
+    EXPECT_EQ(ctx.found, 2);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("separated")), 0);
+    EXPECT_EQ(ctx.pos, 19);
+    EXPECT_EQ(ctx.next_pos, 31);
+    EXPECT_EQ(ctx.delim_pos, 28);
+    EXPECT_EQ(ctx.found, 3);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("string")), 0);
+    EXPECT_EQ(ctx.pos, 31);
+    EXPECT_EQ(ctx.next_pos, 40);
+    EXPECT_EQ(ctx.delim_pos, 37);
+    EXPECT_EQ(ctx.found, 4);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(tok, cx_str("")), 0);
+    EXPECT_EQ(ctx.pos, 40);
+    EXPECT_EQ(ctx.next_pos, 40);
+    EXPECT_EQ(ctx.delim_pos, 40);
+    EXPECT_EQ(ctx.found, 5);
+
+    ret = cx_strtok_next(&ctx, &tok);
+    ASSERT_FALSE(ret);
+    EXPECT_EQ(ctx.pos, 40);
+    EXPECT_EQ(ctx.delim_pos, 40);
+    EXPECT_EQ(ctx.found, 5);
+}
+
+TEST(String, strtok_next_advanced) {
+    cxmutstr str = cx_strdup(cx_str("an,arbitrarily;||separated;string"));
+    cxstring delim = cx_str(",");
+    cxstring delim_more[2] = {CX_STR("||"), CX_STR(";")};
+    CxStrtokCtx ctx = cx_strtok_m(str, delim, 10);
+    cx_strtok_delim(&ctx, delim_more, 2);
+    bool ret;
+    cxmutstr tok;
+
+    ret = cx_strtok_next_m(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("an")), 0);
+    EXPECT_EQ(ctx.pos, 0);
+    EXPECT_EQ(ctx.next_pos, 3);
+    EXPECT_EQ(ctx.delim_pos, 2);
+    EXPECT_EQ(ctx.found, 1);
+    cx_strupper(tok);
+
+    ret = cx_strtok_next_m(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("arbitrarily")), 0);
+    EXPECT_EQ(ctx.pos, 3);
+    EXPECT_EQ(ctx.next_pos, 15);
+    EXPECT_EQ(ctx.delim_pos, 14);
+    EXPECT_EQ(ctx.found, 2);
+    cx_strupper(tok);
+
+    ret = cx_strtok_next_m(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("")), 0);
+    EXPECT_EQ(ctx.pos, 15);
+    EXPECT_EQ(ctx.next_pos, 17);
+    EXPECT_EQ(ctx.delim_pos, 15);
+    EXPECT_EQ(ctx.found, 3);
+    cx_strupper(tok);
+
+    ret = cx_strtok_next_m(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("separated")), 0);
+    EXPECT_EQ(ctx.pos, 17);
+    EXPECT_EQ(ctx.next_pos, 27);
+    EXPECT_EQ(ctx.delim_pos, 26);
+    EXPECT_EQ(ctx.found, 4);
+    cx_strupper(tok);
+
+    ret = cx_strtok_next_m(&ctx, &tok);
+    ASSERT_TRUE(ret);
+    EXPECT_EQ(cx_strcmp(cx_strcast(tok), cx_str("string")), 0);
+    EXPECT_EQ(ctx.pos, 27);
+    EXPECT_EQ(ctx.next_pos, 33);
+    EXPECT_EQ(ctx.delim_pos, 33);
+    EXPECT_EQ(ctx.found, 5);
+    cx_strupper(tok);
+
+    ret = cx_strtok_next_m(&ctx, &tok);
+    ASSERT_FALSE(ret);
+    EXPECT_EQ(ctx.pos, 27);
+    EXPECT_EQ(ctx.next_pos, 33);
+    EXPECT_EQ(ctx.delim_pos, 33);
+    EXPECT_EQ(ctx.found, 5);
+
+    EXPECT_EQ(cx_strcmp(cx_strcast(str), cx_str("AN,ARBITRARILY;||SEPARATED;STRING")), 0);
+
+    cx_strfree(&str);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_tree.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,122 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/tree.h"
+#include <gtest/gtest.h>
+
+struct TestNode {
+    TestNode *parent = nullptr;
+    TestNode *prev = nullptr;
+    TestNode *next = nullptr;
+
+    TestNode *children_begin = nullptr;
+    TestNode *children_end = nullptr;
+};
+
+TEST(Tree, cx_tree_add_sibling) {
+    // prepare test tree
+    TestNode root, a;
+    root.children_begin = &a;
+    root.children_end = &a;
+    a.parent = &root;
+
+    // new test nodes
+    TestNode b, c;
+
+    // test
+    cx_tree_add_sibling(&a, offsetof(TestNode, prev), offsetof(TestNode, next), offsetof(TestNode, parent), &b);
+    EXPECT_EQ(b.parent, &root);
+    EXPECT_EQ(b.prev, &a);
+    EXPECT_EQ(b.next, nullptr);
+    EXPECT_EQ(a.next, &b);
+
+    cx_tree_add_sibling(&a, -1, offsetof(TestNode, next), -1, &c);
+    EXPECT_EQ(c.parent, nullptr);
+    EXPECT_EQ(c.prev, nullptr);
+    EXPECT_EQ(c.next, nullptr);
+    EXPECT_EQ(b.next, &c);
+}
+
+TEST(Tree, cx_tree_add_child) {
+    TestNode root, a, b, c, a1;
+
+    cx_tree_add_child(
+            (void **) &root.children_begin,
+            (void **) &root.children_end,
+            offsetof(TestNode, prev),
+            offsetof(TestNode, next),
+            &a,
+            offsetof(TestNode, parent),
+            &root);
+    EXPECT_EQ(root.children_begin, &a);
+    EXPECT_EQ(root.children_end, &a);
+    EXPECT_EQ(a.parent, &root);
+    EXPECT_EQ(a.prev, nullptr);
+    EXPECT_EQ(a.next, nullptr);
+
+    cx_tree_add_child(
+            (void **) &root.children_begin,
+            (void **) &root.children_end,
+            offsetof(TestNode, prev),
+            offsetof(TestNode, next),
+            &b,
+            offsetof(TestNode, parent),
+            &root);
+    EXPECT_EQ(root.children_begin, &a);
+    EXPECT_EQ(root.children_begin->next, &b);
+    EXPECT_EQ(root.children_end, &b);
+    EXPECT_EQ(b.parent, &root);
+    EXPECT_EQ(b.prev, &a);
+
+    cx_tree_add_child(
+            (void **) &root.children_begin,
+            nullptr,
+            -1,
+            offsetof(TestNode, next),
+            &c,
+            -1,
+            &root);
+    EXPECT_EQ(root.children_end, &b); // children_end unchanged
+    EXPECT_EQ(b.next, &c);
+    EXPECT_EQ(c.prev, nullptr);
+    EXPECT_EQ(c.next, nullptr);
+    EXPECT_EQ(c.parent, nullptr);
+
+    cx_tree_add_child(
+            (void **) &a.children_begin,
+            (void **) &a.children_end,
+            offsetof(TestNode, prev),
+            offsetof(TestNode, next),
+            &a1,
+            offsetof(TestNode, parent),
+            &a);
+    EXPECT_EQ(a.children_begin, &a1);
+    EXPECT_EQ(a1.parent, &a);
+    EXPECT_EQ(root.children_begin, &a);
+    EXPECT_EQ(root.children_begin->children_begin, &a1);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_utils.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,155 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/utils.h"
+
+#include <gtest/gtest.h>
+
+TEST(Utils, ForN) {
+    unsigned j;
+    j = 0;
+    cx_for_n(i, 50) {
+        EXPECT_EQ(i, j);
+        j++;
+    }
+}
+
+TEST(Utils, szmul) {
+    size_t r;
+    int e;
+    e = cx_szmul(5, 7, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(35, r);
+
+    size_t s = SIZE_MAX & ~3;
+
+    e = cx_szmul(s / 4, 2, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s / 2, r);
+    e = cx_szmul(2, s / 4, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s / 2, r);
+
+    e = cx_szmul(s / 4, 4, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s, r);
+
+    e = cx_szmul(4, s / 4, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s, r);
+
+    e = cx_szmul(s / 4, 5, &r);
+    EXPECT_NE(0, e);
+
+    e = cx_szmul(5, s / 4, &r);
+    EXPECT_NE(0, e);
+
+    e = cx_szmul(SIZE_MAX - 4, 0, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul(0, SIZE_MAX - 1, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul(SIZE_MAX, 0, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul(0, SIZE_MAX, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul(0, 0, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+}
+
+#ifdef CX_SZMUL_BUILTIN
+
+// also test the custom implementation
+struct Utils_szmul_impl : ::testing::Test {
+#undef CX_SZMUL_BUILTIN
+
+#include "../src/utils.c"
+
+#define CX_SZMUL_BUILTIN
+};
+
+TEST_F(Utils_szmul_impl, Test) {
+    size_t r;
+    int e;
+    e = cx_szmul_impl(5, 7, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(35, r);
+
+    size_t s = SIZE_MAX & ~3;
+
+    e = cx_szmul_impl(s / 4, 2, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s / 2, r);
+    e = cx_szmul_impl(2, s / 4, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s / 2, r);
+
+    e = cx_szmul_impl(s / 4, 4, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s, r);
+
+    e = cx_szmul_impl(4, s / 4, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(s, r);
+
+    e = cx_szmul_impl(s / 4, 5, &r);
+    EXPECT_NE(0, e);
+
+    e = cx_szmul_impl(5, s / 4, &r);
+    EXPECT_NE(0, e);
+
+    e = cx_szmul_impl(SIZE_MAX - 4, 0, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul_impl(0, SIZE_MAX - 1, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul_impl(SIZE_MAX, 0, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul_impl(0, SIZE_MAX, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+
+    e = cx_szmul_impl(0, 0, &r);
+    EXPECT_EQ(0, e);
+    EXPECT_EQ(0, r);
+}
+
+#endif // CX_SZMUL_BUILTIN
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/util_allocator.cpp	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,167 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "util_allocator.h"
+
+void *cx_malloc_testing(void *d, size_t n) {
+    auto data = reinterpret_cast<CxTestingAllocator *>(d);
+    void *ptr = malloc(n);
+    data->alloc_total++;
+    if (ptr == nullptr) {
+        data->alloc_failed++;
+    } else {
+        data->tracked.insert(ptr);
+    }
+    return ptr;
+}
+
+void *cx_realloc_testing(void *d, void *mem, size_t n) {
+    auto data = reinterpret_cast<CxTestingAllocator *>(d);
+    void *ptr = realloc(mem, n);
+    if (ptr == mem) {
+        return ptr;
+    } else {
+        data->alloc_total++;
+        if (ptr == nullptr) {
+            data->alloc_failed++;
+        } else {
+            data->free_total++;
+            if (data->tracked.erase(mem) == 0) {
+                data->free_failed++;
+            }
+            data->tracked.insert(ptr);
+        }
+        return ptr;
+    }
+}
+
+void *cx_calloc_testing(void *d, size_t nelem, size_t n) {
+    auto data = reinterpret_cast<CxTestingAllocator *>(d);
+    void *ptr = calloc(nelem, n);
+    data->alloc_total++;
+    if (ptr == nullptr) {
+        data->alloc_failed++;
+    } else {
+        data->tracked.insert(ptr);
+    }
+    return ptr;
+}
+
+void cx_free_testing(void *d, void *mem) {
+    auto data = reinterpret_cast<CxTestingAllocator *>(d);
+    data->free_total++;
+    if (data->tracked.erase(mem) == 0) {
+        data->free_failed++;
+        // do not even attempt to free mem, because it is likely to segfault
+    } else {
+        free(mem);
+    }
+}
+
+cx_allocator_class cx_testing_allocator_class = {
+        cx_malloc_testing,
+        cx_realloc_testing,
+        cx_calloc_testing,
+        cx_free_testing
+};
+
+CxTestingAllocator::CxTestingAllocator() : CxAllocator() {
+    cl = &cx_testing_allocator_class;
+    data = this;
+}
+
+bool CxTestingAllocator::used() const {
+    return alloc_total > 0;
+}
+
+bool CxTestingAllocator::verify() const {
+    return tracked.empty() && alloc_failed == 0 && free_failed == 0 && alloc_total == free_total;
+}
+
+// SELF-TEST
+
+#include <gtest/gtest.h>
+
+TEST(TestingAllocator, ExpectFree) {
+    CxTestingAllocator allocator;
+
+    ASSERT_TRUE(allocator.verify());
+    EXPECT_FALSE(allocator.used());
+    auto ptr = cxMalloc(&allocator, 16);
+    EXPECT_TRUE(allocator.used());
+    ASSERT_NE(ptr, nullptr);
+    EXPECT_FALSE(allocator.verify());
+
+    cxFree(&allocator, ptr);
+    EXPECT_TRUE(allocator.verify());
+}
+
+TEST(TestingAllocator, DetectDoubleFree) {
+    CxTestingAllocator allocator;
+
+    ASSERT_TRUE(allocator.verify());
+    auto ptr = cxMalloc(&allocator, 16);
+    ASSERT_NE(ptr, nullptr);
+
+    cxFree(&allocator, ptr);
+    EXPECT_TRUE(allocator.verify());
+    ASSERT_NO_FATAL_FAILURE(cxFree(&allocator, ptr));
+    EXPECT_FALSE(allocator.verify());
+}
+
+TEST(TestingAllocator, FreeUntracked) {
+    CxTestingAllocator allocator;
+
+    auto ptr = malloc(16);
+    ASSERT_TRUE(allocator.verify());
+    ASSERT_NO_FATAL_FAILURE(cxFree(&allocator, ptr));
+    EXPECT_FALSE(allocator.verify());
+    ASSERT_NO_FATAL_FAILURE(free(ptr));
+}
+
+TEST(TestingAllocator, FullLifecycleWithRealloc) {
+    CxTestingAllocator allocator;
+    ASSERT_TRUE(allocator.verify());
+    auto ptr = cxMalloc(&allocator, 16);
+    ASSERT_NE(ptr, nullptr);
+    EXPECT_EQ(allocator.tracked.size(), 1);
+    ptr = cxRealloc(&allocator, ptr, 256);
+    ASSERT_NE(ptr, nullptr);
+    EXPECT_EQ(allocator.tracked.size(), 1);
+    cxFree(&allocator, ptr);
+    EXPECT_TRUE(allocator.verify());
+}
+
+TEST(TestingAllocator, CallocInitializes) {
+    CxTestingAllocator allocator;
+    const char zeros[16] = {0};
+    auto ptr = cxCalloc(&allocator, 16, 1);
+    EXPECT_EQ(memcmp(ptr, zeros, 16), 0);
+    cxFree(&allocator, ptr);
+    EXPECT_TRUE(allocator.verify());
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/util_allocator.h	Tue Feb 07 21:55:37 2023 +0100
@@ -0,0 +1,81 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef UCX_UTIL_ALLOCATOR_H
+#define UCX_UTIL_ALLOCATOR_H
+
+#include "cx/allocator.h"
+
+#include <set>
+
+struct CxTestingAllocator : public CxAllocator {
+    /**
+     * Total number of all allocations (malloc, calloc, realloc).
+     * A realloc() does only count when the memory has to be moved.
+     */
+    unsigned alloc_total = 0;
+    /**
+     * Number of failed allocations (malloc, calloc, realloc).
+     */
+    unsigned alloc_failed = 0;
+    /**
+     * Total number of freed pointers.
+     * A reallocation also counts as a free when the memory has to be moved.
+     */
+    unsigned free_total = 0;
+    /**
+     * Number of failed free invocations.
+     * A free() is considered failed, if it has not been performed on tracked memory.
+     */
+    unsigned free_failed = 0;
+    /**
+     * The set of tracked memory blocks.
+     */
+    std::set<void *> tracked;
+
+    /**
+     * Constructs a new testing allocator.
+     */
+    CxTestingAllocator();
+
+    /**
+     * Verifies that this allocator has been used.
+     *
+     * @return true if any allocation was attempted using this allocator
+     */
+    [[nodiscard]] bool used() const;
+
+    /**
+     * Verifies that all allocated memory blocks are freed and no free occurred twice.
+     *
+     * @return true iff all tracked allocations / deallocations were valid
+     */
+    [[nodiscard]] bool verify() const;
+};
+
+#endif // UCX_UTIL_ALLOCATOR_H

mercurial