add first tests

Tue, 09 Dec 2025 12:13:43 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Tue, 09 Dec 2025 12:13:43 +0100
changeset 23
b26390e77237
parent 22
112b85020dc9
child 24
df671b62538e

add first tests

dbutils/Makefile file | annotate | diff | comparison | revisions
dbutils/db.c file | annotate | diff | comparison | revisions
dbutils/dbutils/db.h file | annotate | diff | comparison | revisions
test/Makefile file | annotate | diff | comparison | revisions
test/database.c file | annotate | diff | comparison | revisions
test/database.h file | annotate | diff | comparison | revisions
test/main.c file | annotate | diff | comparison | revisions
ucx/array_list.c file | annotate | diff | comparison | revisions
ucx/buffer.c file | annotate | diff | comparison | revisions
ucx/cx/array_list.h file | annotate | diff | comparison | revisions
ucx/cx/buffer.h file | annotate | diff | comparison | revisions
ucx/cx/common.h file | annotate | diff | comparison | revisions
ucx/cx/json.h file | annotate | diff | comparison | revisions
ucx/cx/linked_list.h file | annotate | diff | comparison | revisions
ucx/cx/tree.h file | annotate | diff | comparison | revisions
ucx/hash_map.c file | annotate | diff | comparison | revisions
ucx/json.c file | annotate | diff | comparison | revisions
ucx/kv_list.c file | annotate | diff | comparison | revisions
ucx/linked_list.c file | annotate | diff | comparison | revisions
ucx/list.c file | annotate | diff | comparison | revisions
ucx/printf.c file | annotate | diff | comparison | revisions
ucx/properties.c file | annotate | diff | comparison | revisions
ucx/string.c file | annotate | diff | comparison | revisions
ucx/tree.c file | annotate | diff | comparison | revisions
--- a/dbutils/Makefile	Wed Nov 12 18:37:58 2025 +0100
+++ b/dbutils/Makefile	Tue Dec 09 12:13:43 2025 +0100
@@ -40,7 +40,7 @@
 
 LIBDBUTILS = ../build/lib/libdbutils$(LIB_EXT)
 
-all: ../build/ucx $(LIBDBUTILS)
+all: ../build/ucx $(LIBDBUTILS) ../build/testdata.sql
 
 $(LIBDBUTILS): $(OBJ)
 	$(AR) $(ARFLAGS) $(AOFLAGS)$@ $(OBJ)
@@ -51,4 +51,6 @@
 ../build/ucx:
 	test -d '$@'
 
+../build/testdata.sql: ../testdata.sql
+	cp ../testdata.sql ../build/testdata.sql
 
--- a/dbutils/db.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/dbutils/db.c	Tue Dec 09 12:13:43 2025 +0100
@@ -39,6 +39,17 @@
     return err;
 }
 
+int dbuConnectionIsActive(DBUConnection *conn) {
+    return conn->isActive ? conn->isActive(conn) : 1;
+}
+
+void dbuConnectionFree(DBUConnection *conn) {
+    conn->free(conn);
+}
+
+DBUQuery* dbuConnectionQuery(DBUConnection *conn, const CxAllocator *a) {
+    return conn->createQuery(conn, a);
+}
 
 
 int dbuQuerySetSQL(DBUQuery *q, const char *sql) {
--- a/dbutils/dbutils/db.h	Wed Nov 12 18:37:58 2025 +0100
+++ b/dbutils/dbutils/db.h	Tue Dec 09 12:13:43 2025 +0100
@@ -97,6 +97,9 @@
 };
 
 int dbuConnectionExec(DBUConnection *conn, const char *sql);
+int dbuConnectionIsActive(DBUConnection *conn);
+void dbuConnectionFree(DBUConnection *conn);
+DBUQuery* dbuConnectionQuery(DBUConnection *conn, const CxAllocator *a);
 
 int dbuQuerySetSQL(DBUQuery *q, const char *sql);
 int dbuQuerySetParamString(DBUQuery *q, int index, cxstring str);
--- a/test/Makefile	Wed Nov 12 18:37:58 2025 +0100
+++ b/test/Makefile	Tue Dec 09 12:13:43 2025 +0100
@@ -31,7 +31,8 @@
 
 CFLAGS += -I../dbutils/ -I../ucx
 
-SRC = main.c
+SRC  = main.c
+SRC += database.c
 
 OBJ = $(SRC:%.c=../build/test/%$(OBJ_EXT))
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/database.c	Tue Dec 09 12:13:43 2025 +0100
@@ -0,0 +1,196 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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
+ * POSSIBLIITY OF SUCH DAMAGE.
+ */
+
+#include "database.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include <cx/buffer.h>
+#include <cx/streams.h>
+
+#define TEST_DB        "test.db"
+#define TEST_DATA_FILE "testdata.sql"
+
+
+// create test.db and execute testdata script
+int init_test_db(void) {
+    sqlite3 *db;
+    char *err_msg = NULL;
+    
+    FILE *f = fopen(TEST_DATA_FILE, "r");
+    if(!f) {
+        fprintf(stderr, "Cannot open test data file %s: %s\n", TEST_DATA_FILE, strerror(errno));
+    }
+    
+    if(unlink(TEST_DB)) {
+        if(errno != ENOENT) {
+            fprintf(stderr, "Cannot unlink %s: %s\n", TEST_DB, strerror(errno));
+            fclose(f);
+            return 1;
+        }
+    }
+    if(sqlite3_open(TEST_DB, &db) != SQLITE_OK) {
+        fprintf(stderr, "Cannot open database %s: %s\n", TEST_DB, sqlite3_errmsg(db));
+        fclose(f);
+        return 1;
+    }
+    
+    CxBuffer *buf = cxBufferCreate(NULL, 2048, NULL, CX_BUFFER_AUTO_EXTEND | CX_BUFFER_FREE_CONTENTS);
+    cx_stream_copy(f, buf, (cx_read_func)fread, (cx_write_func)cxBufferWrite);
+    int err = 0;
+    if(buf->size > 0) {
+        cxBufferTerminate(buf);
+        
+        if(sqlite3_exec(db, buf->space, 0, 0, &err_msg) != SQLITE_OK) {
+            fprintf(stderr, "SQL error: %s\n", err_msg);
+            sqlite3_free(err_msg);
+            err = 1;
+        }
+    } else {
+        fprintf(stderr, "Error: no file content\n");
+        err = 1;
+    }
+    cxBufferFree(buf);
+    sqlite3_close(db);
+    
+    return err;
+}
+
+
+
+static DBUContext *ctx;
+static DBUConnection *conn;
+
+typedef struct Address {
+    int64_t address_id;
+    
+    cxmutstr street;
+    cxmutstr zip;
+    cxmutstr city;
+} Address;
+
+typedef struct Person {
+    int64_t person_id;
+    
+    cxmutstr name;
+    cxmutstr email;
+    int      age;
+    bool     iscustomer;
+    uint64_t hash;
+    
+    Address *address;
+    
+    CxList *roles;
+} Person;
+
+typedef struct Role {
+    int64_t role_id;
+    int64_t person_id;
+    cxmutstr name;
+} Role;
+
+int init_db_tests(void) {
+    ctx = dbuContextCreate();
+    
+    DBUClass *address = dbuRegisterClass(ctx, "address", Address, address_id);
+    dbuClassAdd(address, Address, street);
+    dbuClassAdd(address, Address, zip);
+    dbuClassAdd(address, Address, city);
+    
+    DBUClass *role = dbuRegisterClass(ctx, "role", Role, role_id);
+    
+    DBUClass *person = dbuRegisterClass(ctx, "person", Person, person_id);
+    dbuClassAdd(person, Person, name);
+    dbuClassAdd(person, Person, email);
+    dbuClassAdd(person, Person, age);
+    dbuClassAdd(person, Person, iscustomer);
+    dbuClassAdd(person, Person, hash);
+    dbuClassAddObj(person, "address_id", offsetof(Person, address), address);
+    dbuClassAddCxLinkedList(person, "person_id", offsetof(Person, roles), role);
+    
+    dbuClassAddForeignKey(role, Role, person_id, person);
+    dbuClassAdd(role, Role, name);
+    
+    return 0;
+}
+
+void cleanup_db_tests(void) {
+    if(conn) {
+        dbuConnectionFree(conn);
+    }
+    dbuContextFree(ctx);
+}
+
+CX_TEST(testSqliteConnection) {
+    CX_TEST_DO {
+        conn = dbuSQLiteConnection(TEST_DB);
+        CX_TEST_ASSERT(conn);
+        CX_TEST_ASSERT(dbuConnectionIsActive(conn));
+    }
+}
+
+CX_TEST(testConnectionExec) {
+    CX_TEST_DO {
+        CX_TEST_ASSERT(conn);
+        
+        int t1 = dbuConnectionExec(conn, "create table ExecTest1(a int);");
+        CX_TEST_ASSERT(t1 == 0);
+        
+        int t2 = dbuConnectionExec(conn, "insert into ExecTest1(a) values (1);");
+        CX_TEST_ASSERT(t2 == 0);
+        
+        int t3 = dbuConnectionExec(conn, "drop table ExecTest1;");
+        CX_TEST_ASSERT(t3 == 0);
+        
+        int fail = dbuConnectionExec(conn, "select * from Fail;");
+        CX_TEST_ASSERT(fail != 0);
+    }
+}
+
+CX_TEST(testSingleValueQuery) {
+    CX_TEST_DO {
+        CX_TEST_ASSERT(conn);
+        
+        DBUQuery *q = dbuConnectionQuery(conn, NULL);
+        CX_TEST_ASSERT(q);
+        CX_TEST_ASSERT(dbuQuerySetSQL(q, "select 12;") == 0);
+        CX_TEST_ASSERT(dbuQueryExec(q) == 0);
+        
+        DBUResult *r = dbuQueryGetResult(q);
+        CX_TEST_ASSERT(r);
+        int value;
+        CX_TEST_ASSERT(dbuResultAsValue(r, &value) == 0);
+        CX_TEST_ASSERT(value == 12);
+        
+        dbuQueryFree(q);
+        
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/database.h	Tue Dec 09 12:13:43 2025 +0100
@@ -0,0 +1,61 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 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
+ * POSSIBLIITY OF SUCH DAMAGE.
+ */
+
+#ifndef TEST_DATABASE_H
+#define TEST_DATABASE_H
+
+#ifdef DBU_SQLITE
+
+#include <cx/test.h>
+#include <dbutils/dbutils.h>
+#include <dbutils/sqlite.h>
+#include <dbutils/db.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+   
+int init_test_db(void);
+
+int init_db_tests(void);
+
+void cleanup_db_tests(void);
+
+CX_TEST(testSqliteConnection);
+CX_TEST(testConnectionExec);
+CX_TEST(testSingleValueQuery);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* DBU_SQLITE */
+
+#endif /* TEST_DATABASE_H */
+
--- a/test/main.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/test/main.c	Tue Dec 09 12:13:43 2025 +0100
@@ -33,9 +33,12 @@
 #include <dbutils/sqlite.h>
 #include <dbutils/db.h>
 
+#include <cx/test.h>
 #include <cx/buffer.h>
 #include <cx/printf.h>
 
+#include "database.h"
+
 const char *sql_create_table_person =
 "create table if not exists Person ("
 "person_id integer primary key autoincrement, "
@@ -115,7 +118,26 @@
 static int create_test_data(sqlite3 *db);
 
 int main(int argc, char **argv) {
+    CxTestSuite *suite = cx_test_suite_new("dbu");
     
+#ifdef DBU_SQLITE
+    if(init_test_db() || init_db_tests()) {
+        return 1;
+    }
+    
+    cx_test_register(suite, testSqliteConnection);
+    cx_test_register(suite, testConnectionExec);
+    cx_test_register(suite, testSingleValueQuery);
+#endif
+    
+    cx_test_run_stdout(suite);
+    
+    
+#ifdef DBU_SQLITE
+    cleanup_db_tests();
+#endif
+    
+    /*
     
     DBUContext *ctx = dbuContextCreate();
     
@@ -209,6 +231,8 @@
     
     conn->free(conn);
     
+    */
+     
     return 0;
 }
 
--- a/ucx/array_list.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/array_list.c	Tue Dec 09 12:13:43 2025 +0100
@@ -42,10 +42,11 @@
         cx_attr_unused CxArrayReallocator *alloc
 ) {
     size_t n;
+    // LCOV_EXCL_START
     if (cx_szmul(new_capacity, elem_size, &n)) {
         errno = EOVERFLOW;
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
     return cxReallocDefault(array, n);
 }
 
@@ -66,10 +67,11 @@
 ) {
     // check for overflow
     size_t n;
+    // LCOV_EXCL_START
     if (cx_szmul(new_capacity, elem_size, &n)) {
         errno = EOVERFLOW;
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
 
     // retrieve the pointer to the actual allocator
     const CxAllocator *al = alloc->allocator;
@@ -108,13 +110,11 @@
  *
  * @param current_capacity the current capacity of the array
  * @param needed_capacity the required capacity of the array
- * @param maximum_capacity the maximum capacity (given by the data type)
  * @return the new capacity
  */
 static size_t cx_array_grow_capacity(
     size_t current_capacity,
-    size_t needed_capacity,
-    size_t maximum_capacity
+    size_t needed_capacity
 ) {
     if (current_capacity >= needed_capacity) {
         return current_capacity;
@@ -125,12 +125,7 @@
     else if (cap < 1024) alignment = 64;
     else if (cap < 8192) alignment = 512;
     else alignment = 1024;
-
-    if (cap - 1 > maximum_capacity - alignment) {
-        return maximum_capacity;
-    } else {
-        return cap - (cap % alignment) + alignment;
-    }
+    return cap - (cap % alignment) + alignment;
 }
 
 int cx_array_reserve(
@@ -288,7 +283,7 @@
     const size_t newsize = oldsize < minsize ? minsize : oldsize;
 
     // reallocate if necessary
-    const size_t newcap = cx_array_grow_capacity(oldcap, newsize, max_size);
+    const size_t newcap = cx_array_grow_capacity(oldcap, newsize);
     if (newcap > oldcap) {
         // check if we need to repair the src pointer
         uintptr_t targetaddr = (uintptr_t) *target;
@@ -372,17 +367,18 @@
     if (elem_count == 0) return 0;
 
     // overflow check
+    // LCOV_EXCL_START
     if (elem_count > SIZE_MAX - *size) {
         errno = EOVERFLOW;
         return 1;
     }
+    // LCOV_EXCL_STOP
 
     // store some counts
     const size_t old_size = *size;
     const size_t old_capacity = *capacity;
     // the necessary capacity is the worst case assumption, including duplicates
-    const size_t needed_capacity = cx_array_grow_capacity(old_capacity,
-        old_size + elem_count, SIZE_MAX);
+    const size_t needed_capacity = cx_array_grow_capacity(old_capacity, old_size + elem_count);
 
     // if we need more than we have, try a reallocation
     if (needed_capacity > old_capacity) {
@@ -421,21 +417,23 @@
     // while there are both source and buffered elements left,
     // copy them interleaving
     while (si < elem_count && bi < new_size) {
-        // determine how many source elements can be inserted
+        // determine how many source elements can be inserted.
+        // the first element that shall not be inserted is the smallest element
+        // that is strictly larger than the first buffered element
+        // (located at the index of the infimum plus one).
+        // the infimum is guaranteed to exist:
+        // - if all src elements are larger,
+        //   there is no buffer, and this loop is skipped
+        // - if any src element is smaller or equal, the infimum exists
+        // - when all src elements that are smaller are copied, the second part
+        //   of this loop body will copy the remaining buffer (emptying it)
+        // Therefore, the buffer can never contain an element that is smaller
+        // than any element in the source and the infimum exists.
         size_t copy_len, bytes_copied;
-        copy_len = cx_array_binary_search_sup(
-                src,
-                elem_count - si,
-                elem_size,
-                bptr,
-                cmp_func
+        copy_len = cx_array_binary_search_inf(
+            src, elem_count - si, elem_size, bptr, cmp_func
         );
-        // binary search gives us the smallest index;
-        // we also want to include equal elements here
-        while (si + copy_len < elem_count
-                && cmp_func(bptr, src+copy_len*elem_size) == 0) {
-            copy_len++;
-        }
+        copy_len++;
 
         // copy the source elements
         if (copy_len > 0) {
@@ -512,26 +510,17 @@
             // duplicates allowed or nothing inserted yet: simply copy everything
             memcpy(dest, src, elem_size * (elem_count - si));
         } else {
-            if (dest != *target) {
-                // skip all source elements that equal the last element
-                char *last = dest - elem_size;
-                while (si < elem_count) {
-                    if (last != NULL && cmp_func(last, src) == 0) {
-                        src += elem_size;
-                        si++;
-                        (*size)--;
-                    } else {
-                        break;
-                    }
-                }
-            }
-            // we must check the elements in the chunk one by one
+            // we must check the remaining source elements one by one
+            // to skip the duplicates.
+            // Note that no source element can equal the last element in the
+            // destination, because that would have created an insertion point
+            // and a buffer, s.t. the above loop already handled the duplicates
             while (si < elem_count) {
                 // find a chain of elements that can be copied
                 size_t copy_len = 1, skip_len = 0;
                 {
                     const char *left_src = src;
-                    while (si + copy_len < elem_count) {
+                    while (si + copy_len + skip_len < elem_count) {
                         const char *right_src = left_src + elem_size;
                         int d = cmp_func(left_src,  right_src);
                         if (d < 0) {
@@ -599,7 +588,8 @@
         cmp_func, sorted_data, elem_size, elem_count, reallocator, false);
 }
 
-size_t cx_array_binary_search_inf(
+// implementation that finds ANY index
+static size_t cx_array_binary_search_inf_impl(
         const void *arr,
         size_t size,
         size_t elem_size,
@@ -644,13 +634,6 @@
         result = cmp_func(elem, arr_elem);
         if (result == 0) {
             // found it!
-            // check previous elements;
-            // when they are equal, report the smallest index
-            arr_elem -= elem_size;
-            while (pivot_index > 0 && cmp_func(elem, arr_elem) == 0) {
-                pivot_index--;
-                arr_elem -= elem_size;
-            }
             return pivot_index;
         } else if (result < 0) {
             // element is smaller than pivot, continue search left
@@ -665,6 +648,24 @@
     return result < 0 ? (pivot_index - 1) : pivot_index;
 }
 
+size_t cx_array_binary_search_inf(
+        const void *arr,
+        size_t size,
+        size_t elem_size,
+        const void *elem,
+        cx_compare_func cmp_func
+) {
+    size_t index = cx_array_binary_search_inf_impl(
+        arr, size, elem_size, elem, cmp_func);
+    // in case of equality, report the largest index
+    const char *e = ((const char *) arr) + (index + 1) * elem_size;
+    while (index + 1 < size && cmp_func(e, elem) == 0) {
+        e += elem_size;
+        index++;
+    }
+    return index;
+}
+
 size_t cx_array_binary_search(
         const void *arr,
         size_t size,
@@ -690,16 +691,25 @@
         const void *elem,
         cx_compare_func cmp_func
 ) {
-    size_t inf = cx_array_binary_search_inf(
+    size_t index = cx_array_binary_search_inf_impl(
             arr, size, elem_size, elem, cmp_func
     );
-    if (inf == size) {
-        // no infimum means, first element is supremum
+    const char *e = ((const char *) arr) + index * elem_size;
+    if (index == size) {
+        // no infimum means the first element is supremum
         return 0;
-    } else if (cmp_func(((const char *) arr) + inf * elem_size, elem) == 0) {
-        return inf;
+    } else if (cmp_func(e, elem) == 0) {
+        // found an equal element, search the smallest index
+        e -= elem_size; // e now contains the element at index-1
+        while (index > 0 && cmp_func(e, elem) == 0) {
+            e -= elem_size;
+            index--;
+        }
+        return index;
     } else {
-        return inf + 1;
+        // we already have the largest index of the infimum (by design)
+        // the next element is the supremum (or there is no supremum)
+        return index + 1;
     }
 }
 
@@ -792,14 +802,13 @@
 
     // guarantee enough capacity
     if (arl->capacity < list->collection.size + n) {
-        const size_t new_capacity = cx_array_grow_capacity(arl->capacity,
-            list->collection.size + n, SIZE_MAX);
+        const size_t new_capacity = cx_array_grow_capacity(arl->capacity,list->collection.size + n);
         if (cxReallocateArray(
                 list->collection.allocator,
                 &arl->data, new_capacity,
                 list->collection.elem_size)
         ) {
-            return 0;
+            return 0; // LCOV_EXCL_LINE
         }
         arl->capacity = new_capacity;
     }
@@ -843,7 +852,7 @@
             &arl->reallocator
     )) {
         // array list implementation is "all or nothing"
-        return 0;
+        return 0;  // LCOV_EXCL_LINE
     } else {
         return n;
     }
@@ -868,7 +877,7 @@
             &arl->reallocator
     )) {
         // array list implementation is "all or nothing"
-        return 0;
+        return 0;  // LCOV_EXCL_LINE
     } else {
         return n;
     }
@@ -895,7 +904,7 @@
     if (iter->index < list->collection.size) {
         if (cx_arl_insert_element(list,
                 iter->index + 1 - prepend, elem) == NULL) {
-            return 1;
+            return 1; // LCOV_EXCL_LINE
         }
         iter->elem_count++;
         if (prepend != 0) {
@@ -905,7 +914,7 @@
         return 0;
     } else {
         if (cx_arl_insert_element(list, list->collection.size, elem) == NULL) {
-            return 1;
+            return 1;  // LCOV_EXCL_LINE
         }
         iter->elem_count++;
         iter->index = list->collection.size;
--- a/ucx/buffer.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/buffer.c	Tue Dec 09 12:13:43 2025 +0100
@@ -35,7 +35,7 @@
 #ifdef _WIN32
 #include <Windows.h>
 #include <sysinfoapi.h>
-static unsigned long system_page_size() {
+static unsigned long system_page_size(void) {
     static unsigned long ps = 0;
     if (ps == 0) {
         SYSTEM_INFO sysinfo;
@@ -44,16 +44,27 @@
     }
     return ps;
 }
-#define SYSTEM_PAGE_SIZE system_page_size()
 #else
 #include <unistd.h>
-#define SYSTEM_PAGE_SIZE sysconf(_SC_PAGESIZE)
+static unsigned long system_page_size(void) {
+    static unsigned long ps = 0;
+    if (ps == 0) {
+        long sc = sysconf(_SC_PAGESIZE);
+        if (sc < 0) {
+            // fallback for systems which do not report a value here
+            ps = 4096; // LCOV_EXCL_LINE
+        } else {
+            ps = (unsigned long) sc;
+        }
+    }
+    return ps;
+}
 #endif
 
 static int buffer_copy_on_write(CxBuffer* buffer) {
     if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) return 0;
     void *newspace = cxMalloc(buffer->allocator, buffer->capacity);
-    if (NULL == newspace) return -1;
+    if (NULL == newspace) return -1;  // LCOV_EXCL_LINE
     memcpy(newspace, buffer->space, buffer->size);
     buffer->space = newspace;
     buffer->flags &= ~CX_BUFFER_COPY_ON_WRITE;
@@ -78,9 +89,7 @@
     buffer->flags = flags;
     if (!space) {
         buffer->bytes = cxMalloc(allocator, capacity);
-        if (buffer->bytes == NULL) {
-            return -1; // LCOV_EXCL_LINE
-        }
+        if (buffer->bytes == NULL) return -1; // LCOV_EXCL_LINE
         buffer->flags |= CX_BUFFER_FREE_CONTENTS;
     } else {
         buffer->bytes = space;
@@ -122,7 +131,7 @@
         allocator = cxDefaultAllocator;
     }
     CxBuffer *buf = cxMalloc(allocator, sizeof(CxBuffer));
-    if (buf == NULL) return NULL;
+    if (buf == NULL) return NULL; // LCOV_EXCL_LINE
     if (0 == cxBufferInit(buf, space, capacity, allocator, flags)) {
         return buf;
     } else {
@@ -183,6 +192,35 @@
 
 }
 
+size_t cxBufferPop(CxBuffer *buffer, size_t size, size_t nitems) {
+    size_t len;
+    if (cx_szmul(size, nitems, &len)) {
+        // LCOV_EXCL_START
+        errno = EOVERFLOW;
+        return 0;
+        // LCOV_EXCL_STOP
+    }
+    if (len == 0) return 0;
+    if (len > buffer->size) {
+        if (size == 1) {
+            // simple case: everything can be discarded
+            len = buffer->size;
+        } else {
+            // complicated case: misaligned bytes must stay
+            size_t misalignment = buffer->size % size;
+            len = buffer->size - misalignment;
+        }
+    }
+    buffer->size -= len;
+
+    // adjust position, if required
+    if (buffer->pos > buffer->size) {
+        buffer->pos = buffer->size;
+    }
+
+    return len / size;
+}
+
 void cxBufferClear(CxBuffer *buffer) {
     if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) {
         memset(buffer->bytes, 0, buffer->size);
@@ -200,36 +238,10 @@
     return buffer->pos >= buffer->size;
 }
 
-int cxBufferMinimumCapacity(
-        CxBuffer *buffer,
-        size_t newcap
-) {
+int cxBufferReserve(CxBuffer *buffer, size_t newcap) {
     if (newcap <= buffer->capacity) {
         return 0;
     }
-
-    unsigned long pagesize = SYSTEM_PAGE_SIZE;
-    // if page size is larger than 64 KB - for some reason - truncate to 64 KB
-    if (pagesize > 65536) pagesize = 65536;
-    if (newcap < pagesize) {
-        // when smaller as one page, map to the next power of two
-        newcap--;
-        newcap |= newcap >> 1;
-        newcap |= newcap >> 2;
-        newcap |= newcap >> 4;
-        // last operation only needed for pages larger 4096 bytes
-        // but if/else would be more expensive than just doing this
-        newcap |= newcap >> 8;
-        newcap++;
-    } else {
-        // otherwise, map to a multiple of the page size
-        newcap -= newcap % pagesize;
-        newcap += pagesize;
-        // note: if newcap is already page aligned,
-        // this gives a full additional page (which is good)
-    }
-
-
     const int force_copy_flags = CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_COPY_ON_EXTEND;
     if (buffer->flags & force_copy_flags) {
         void *newspace = cxMalloc(buffer->allocator, newcap);
@@ -249,6 +261,38 @@
     }
 }
 
+static size_t cx_buffer_calculate_minimum_capacity(size_t mincap) {
+    unsigned long pagesize = system_page_size();
+    // if page size is larger than 64 KB - for some reason - truncate to 64 KB
+    if (pagesize > 65536) pagesize = 65536;
+    if (mincap < pagesize) {
+        // when smaller as one page, map to the next power of two
+        mincap--;
+        mincap |= mincap >> 1;
+        mincap |= mincap >> 2;
+        mincap |= mincap >> 4;
+        // last operation only needed for pages larger 4096 bytes
+        // but if/else would be more expensive than just doing this
+        mincap |= mincap >> 8;
+        mincap++;
+    } else {
+        // otherwise, map to a multiple of the page size
+        mincap -= mincap % pagesize;
+        mincap += pagesize;
+        // note: if newcap is already page aligned,
+        // this gives a full additional page (which is good)
+    }
+    return mincap;
+}
+
+int cxBufferMinimumCapacity(CxBuffer *buffer, size_t newcap) {
+    if (newcap <= buffer->capacity) {
+        return 0;
+    }
+    newcap = cx_buffer_calculate_minimum_capacity(newcap);
+    return cxBufferReserve(buffer, newcap);
+}
+
 void cxBufferShrink(
         CxBuffer *buffer,
         size_t reserve
@@ -351,8 +395,17 @@
     bool perform_flush = false;
     if (required > buffer->capacity) {
         if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-            if (buffer->flush != NULL && required > buffer->flush->threshold) {
-                perform_flush = true;
+            if (buffer->flush != NULL) {
+                size_t newcap = cx_buffer_calculate_minimum_capacity(required);
+                if (newcap > buffer->flush->threshold) {
+                    newcap = buffer->flush->threshold;
+                }
+                if (cxBufferReserve(buffer, newcap)) {
+                    return total_flushed; // LCOV_EXCL_LINE
+                }
+                if (required > newcap) {
+                    perform_flush = true;
+                }
             } else {
                 if (cxBufferMinimumCapacity(buffer, required)) {
                     return total_flushed; // LCOV_EXCL_LINE
--- a/ucx/cx/array_list.h	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/cx/array_list.h	Tue Dec 09 12:13:43 2025 +0100
@@ -676,6 +676,9 @@
  * in @p arr that is less or equal to @p elem with respect to @p cmp_func.
  * When no such element exists, @p size is returned.
  *
+ * When such an element exists more than once, the largest index of all those
+ * elements is returned.
+ *
  * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
@@ -698,6 +701,9 @@
 /**
  * Searches an item in a sorted array.
  *
+ * When such an element exists more than once, the largest index of all those
+ * elements is returned.
+ *
  * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
@@ -722,6 +728,9 @@
  * in @p arr that is greater or equal to @p elem with respect to @p cmp_func.
  * When no such element exists, @p size is returned.
  *
+ * When such an element exists more than once, the smallest index of all those
+ * elements is returned.
+ *
  * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
--- a/ucx/cx/buffer.h	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/cx/buffer.h	Tue Dec 09 12:13:43 2025 +0100
@@ -118,7 +118,7 @@
     /**
      * The maximum number of blocks to flush in one cycle.
      *
-     * @attention while it is guaranteed that cxBufferFlush() will not flush
+     * @attention While it is guaranteed that cxBufferFlush() will not flush
      * more blocks, this is not necessarily the case for cxBufferWrite().
      * After performing a flush cycle, cxBufferWrite() will retry the write
      * operation and potentially trigger another flush cycle, until the
@@ -388,6 +388,20 @@
 CX_EXPORT int cxBufferSeek(CxBuffer *buffer, off_t offset, int whence);
 
 /**
+ * Discards items from the end of the buffer.
+ *
+ * When the current position points to a byte that gets discarded,
+ * the position is set to the buffer size.
+ *
+ * @param buffer the buffer
+ * @param size the size of one item
+ * @param nitems the number of items to discard
+ * @return the actual number of discarded items
+ */
+cx_attr_nonnull
+CX_EXPORT size_t cxBufferPop(CxBuffer *buffer, size_t size, size_t nitems);
+
+/**
  * Clears the buffer by resetting the position and deleting the data.
  *
  * The data is deleted by zeroing it with a call to memset().
@@ -425,11 +439,30 @@
 cx_attr_nonnull cx_attr_nodiscard
 CX_EXPORT bool cxBufferEof(const CxBuffer *buffer);
 
+/**
+ * Ensures that the buffer has the required capacity.
+ *
+ * If the current capacity is not sufficient, the buffer will be extended.
+ *
+ * This function will reserve no more bytes than requested, in contrast to
+ * cxBufferMinimumCapacity(), which may reserve more bytes to improve the
+ * number of future necessary reallocations.
+ *
+ * @param buffer the buffer
+ * @param capacity the required capacity for this buffer
+ * @retval zero the capacity was already sufficient or successfully increased
+ * @retval non-zero on allocation failure
+ * @see cxBufferShrink()
+ * @see cxBufferMinimumCapacity()
+ */
+cx_attr_nonnull
+CX_EXPORT int cxBufferReserve(CxBuffer *buffer, size_t capacity);
 
 /**
  * Ensures that the buffer has a minimum capacity.
  *
- * If the current capacity is not sufficient, the buffer will be extended.
+ * If the current capacity is not sufficient, the buffer will be generously
+ * extended.
  *
  * The new capacity will be a power of two until the system's page size is reached.
  * Then, the new capacity will be a multiple of the page size.
@@ -438,6 +471,7 @@
  * @param capacity the minimum required capacity for this buffer
  * @retval zero the capacity was already sufficient or successfully increased
  * @retval non-zero on allocation failure
+ * @see cxBufferReserve()
  * @see cxBufferShrink()
  */
 cx_attr_nonnull
@@ -457,6 +491,7 @@
  *
  * @param buffer the buffer
  * @param reserve the number of bytes that shall remain reserved
+ * @see cxBufferReserve()
  * @see cxBufferMinimumCapacity()
  */
 cx_attr_nonnull
--- a/ucx/cx/common.h	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/cx/common.h	Tue Dec 09 12:13:43 2025 +0100
@@ -80,10 +80,10 @@
 #define UCX_COMMON_H
 
 /** Major UCX version as integer constant. */
-#define UCX_VERSION_MAJOR   3
+#define UCX_VERSION_MAJOR   4
 
 /** Minor UCX version as integer constant. */
-#define UCX_VERSION_MINOR   1
+#define UCX_VERSION_MINOR   0
 
 /** Version constant which ensures to increase monotonically. */
 #define UCX_VERSION (((UCX_VERSION_MAJOR)<<16)|UCX_VERSION_MINOR)
@@ -284,6 +284,9 @@
  */
 #define CX_INLINE __attribute__((always_inline)) static inline
 #else
+/**
+ * Declares a function to be inlined.
+ */
 #define CX_INLINE static inline
 #endif
 /**
--- a/ucx/cx/json.h	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/cx/json.h	Tue Dec 09 12:13:43 2025 +0100
@@ -556,7 +556,7 @@
  * @retval non-zero internal allocation error
  * @see cxJsonFill()
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
+cx_attr_nonnull_arg(1) cx_attr_access_r(2, 3)
 CX_EXPORT int cxJsonFilln(CxJson *json, const char *buf, size_t len);
 
 
@@ -641,28 +641,27 @@
 /**
  * Creates a new JSON string.
  *
+ * Internal function - use cxJsonCreateString() instead.
+ *
  * @param allocator the allocator to use
  * @param str the string data
  * @return the new JSON value or @c NULL if allocation fails
- * @see cxJsonCreateString()
  * @see cxJsonObjPutString()
- * @see cxJsonArrAddStrings()
+ * @see cxJsonArrAddCxStrings()
  */
-cx_attr_nodiscard cx_attr_nonnull_arg(2) cx_attr_cstr_arg(2)
-CX_EXPORT CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char *str);
+cx_attr_nodiscard
+CX_EXPORT CxJsonValue* cx_json_create_string(const CxAllocator* allocator, cxstring str);
 
 /**
  * Creates a new JSON string.
  *
- * @param allocator the allocator to use
- * @param str the string data
- * @return the new JSON value or @c NULL if allocation fails
- * @see cxJsonCreateCxString()
- * @see cxJsonObjPutCxString()
+ * @param allocator (@c CxAllocator*) the allocator to use
+ * @param str the string
+ * @return (@c CxJsonValue*) the new JSON value or @c NULL if allocation fails
+ * @see cxJsonObjPutString()
  * @see cxJsonArrAddCxStrings()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str);
+#define cxJsonCreateString(allocator, str) cx_json_create_string(allocator, cx_strcast(str))
 
 /**
  * Creates a new JSON literal.
@@ -760,10 +759,7 @@
 /**
  * Adds or replaces a value within a JSON object.
  *
- * The value will be directly added and not copied.
- *
- * @note If a value with the specified @p name already exists,
- * it will be (recursively) freed with its own allocator.
+ * Internal function - use cxJsonObjPut().
  *
  * @param obj the JSON object
  * @param name the name of the value
@@ -772,11 +768,29 @@
  * @retval non-zero allocation failure
  */
 cx_attr_nonnull
-CX_EXPORT int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child);
+CX_EXPORT int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child);
+
+/**
+ * Adds or replaces a value within a JSON object.
+ *
+ * The value will be directly added and not copied.
+ *
+ * @note If a value with the specified @p name already exists,
+ * it will be (recursively) freed with its own allocator.
+ *
+ * @param obj (@c CxJsonValue*) the JSON object
+ * @param name (any string) the name of the value
+ * @param child (@c CxJsonValue*) the value
+ * @retval zero success
+ * @retval non-zero allocation failure
+ */
+#define cxJsonObjPut(obj, name, child) cx_json_obj_put(obj, cx_strcast(name), child)
 
 /**
  * Creates a new JSON object and adds it to an existing object.
  *
+ * Internal function - use cxJsonObjPutObj().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @return the new value or @c NULL if allocation fails
@@ -784,11 +798,24 @@
  * @see cxJsonCreateObj()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name);
+CX_EXPORT CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name);
+
+/**
+ * Creates a new JSON object and adds it to an existing object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateObj()
+ */
+#define cxJsonObjPutObj(obj, name) cx_json_obj_put_obj(obj, cx_strcast(name))
 
 /**
  * Creates a new JSON array and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutArr().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @return the new value or @c NULL if allocation fails
@@ -796,11 +823,24 @@
  * @see cxJsonCreateArr()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name);
+CX_EXPORT CxJsonValue* cx_json_obj_put_arr(CxJsonValue* obj, cxstring name);
+
+/**
+ * Creates a new JSON array and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateArr()
+ */
+#define cxJsonObjPutArr(obj, name) cx_json_obj_put_arr(obj, cx_strcast(name))
 
 /**
  * Creates a new JSON number and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutNumber().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param num the numeric value
@@ -809,11 +849,25 @@
  * @see cxJsonCreateNumber()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num);
+CX_EXPORT CxJsonValue* cx_json_obj_put_number(CxJsonValue* obj, cxstring name, double num);
+
+/**
+ * Creates a new JSON number and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param num (@c double) the numeric value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateNumber()
+ */
+#define cxJsonObjPutNumber(obj, name, num) cx_json_obj_put_number(obj, cx_strcast(name), num)
 
 /**
  * Creates a new JSON number, based on an integer, and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutInteger().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param num the numeric value
@@ -822,12 +876,24 @@
  * @see cxJsonCreateInteger()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num);
+CX_EXPORT CxJsonValue* cx_json_obj_put_integer(CxJsonValue* obj, cxstring name, int64_t num);
+
+/**
+ * Creates a new JSON number, based on an integer, and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param num (@c int64_t) the numeric value
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateInteger()
+ */
+#define cxJsonObjPutInteger(obj, name, num) cx_json_obj_put_integer(obj, cx_strcast(name), num)
 
 /**
  * Creates a new JSON string and adds it to an object.
  *
- * The string data is copied.
+ * Internal function - use cxJsonObjPutString()
  *
  * @param obj the target JSON object
  * @param name the name of the new value
@@ -836,27 +902,28 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateString()
  */
-cx_attr_nonnull cx_attr_cstr_arg(3)
-CX_EXPORT CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str);
+cx_attr_nonnull
+CX_EXPORT CxJsonValue* cx_json_obj_put_string(CxJsonValue* obj, cxstring name, cxstring str);
 
 /**
  * Creates a new JSON string and adds it to an object.
  *
  * The string data is copied.
  *
- * @param obj the target JSON object
- * @param name the name of the new value
- * @param str the string data
- * @return the new value or @c NULL if allocation fails
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param str (any string) the string data
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
- * @see cxJsonCreateCxString()
+ * @see cxJsonCreateString()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str);
+#define cxJsonObjPutString(obj, name, str) cx_json_obj_put_string(obj, cx_strcast(name), cx_strcast(str))
 
 /**
  * Creates a new JSON literal and adds it to an object.
  *
+ * Internal function - use cxJsonObjPutLiteral().
+ *
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param lit the type of literal
@@ -865,7 +932,19 @@
  * @see cxJsonCreateLiteral()
  */
 cx_attr_nonnull
-CX_EXPORT CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
+CX_EXPORT CxJsonValue* cx_json_obj_put_literal(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
+
+/**
+ * Creates a new JSON literal and adds it to an object.
+ *
+ * @param obj (@c CxJsonValue*) the target JSON object
+ * @param name (any string) the name of the new value
+ * @param lit (@c CxJsonLiteral) the type of literal
+ * @return (@c CxJsonValue*) the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateLiteral()
+ */
+#define cxJsonObjPutLiteral(obj, name, lit) cx_json_obj_put_literal(obj, cx_strcast(name), lit)
 
 /**
  * Recursively deallocates the memory of a JSON value.
@@ -1188,6 +1267,20 @@
 CX_EXPORT CxIterator cxJsonArrIter(const CxJsonValue *value);
 
 /**
+ * Returns the size of a JSON object.
+ *
+ * If the @p value is not a JSON object, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return the size of the object, i.e., the number of key/value pairs
+ * @see cxJsonIsObject()
+ */
+cx_attr_nonnull
+CX_INLINE size_t cxJsonObjSize(const CxJsonValue *value) {
+    return value->value.object.values_size;
+}
+
+/**
  * Returns an iterator over the JSON object members.
  *
  * The iterator yields values of type @c CxJsonObjValue* which
--- a/ucx/cx/linked_list.h	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/cx/linked_list.h	Tue Dec 09 12:13:43 2025 +0100
@@ -62,7 +62,14 @@
      */
     off_t loc_data;
     /**
+     * Location of extra data (optional).
+     * Negative when no extra data is requested.
+     * @see cx_linked_list_extra_data()
+     */
+    off_t loc_extra;
+    /**
      * Additional bytes to allocate @em behind the payload (e.g. for metadata).
+     * @see cx_linked_list_extra_data()
      */
     size_t extra_data_len;
     /**
@@ -112,6 +119,23 @@
         cxLinkedListCreate(NULL, NULL, elem_size)
 
 /**
+ * Instructs the linked list to reserve extra data in each node.
+ *
+ * The extra data will be aligned and placed behind the element data.
+ * The exact location in the node is stored in the @c loc_extra field
+ * of the linked list.
+ *
+ * You should usually not use this function except when you are creating an
+ * own linked-list implementation that is based on the UCX linked list and
+ * needs to store extra data in each node.
+ *
+ * @param list the list (must be a linked list)
+ * @param len the length of the extra data
+ */
+cx_attr_nonnull
+CX_EXPORT void cx_linked_list_extra_data(cx_linked_list *list, size_t len);
+
+/**
  * Finds the node at a certain index.
  *
  * This function can be used to start at an arbitrary position within the list.
--- a/ucx/cx/tree.h	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/cx/tree.h	Tue Dec 09 12:13:43 2025 +0100
@@ -88,6 +88,7 @@
     ptrdiff_t loc_next;
     /**
      * The total number of distinct nodes that have been passed so far.
+     * This includes the current node.
      */
     size_t counter;
     /**
@@ -185,6 +186,7 @@
     ptrdiff_t loc_next;
     /**
      * The total number of distinct nodes that have been passed so far.
+     * This includes the currently visited node.
      */
     size_t counter;
     /**
@@ -1094,7 +1096,7 @@
  * @see cxTreeIterate()
  */
 cx_attr_nonnull cx_attr_nodiscard
-CxTreeVisitor cxTreeVisit(CxTree *tree);
+CX_EXPORT CxTreeVisitor cxTreeVisit(CxTree *tree);
 
 /**
  * Sets the (new) parent of the specified child.
--- a/ucx/hash_map.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/hash_map.c	Tue Dec 09 12:13:43 2025 +0100
@@ -117,7 +117,7 @@
                 allocator,
                 sizeof(struct cx_hash_map_element_s) + map->collection.elem_size
         );
-        if (e == NULL) return NULL;
+        if (e == NULL) return NULL; // LCOV_EXCL_LINE
 
         // write the value
         if (value == NULL) {
@@ -130,10 +130,10 @@
 
         // copy the key
         void *kd = cxMalloc(allocator, key.len);
-        if (kd == NULL) {
+        if (kd == NULL) { // LCOV_EXCL_START
             cxFree(allocator, e);
             return NULL;
-        }
+        } // LCOV_EXCL_STOP
         memcpy(kd, key.data, key.len);
         e->key.data = kd;
         e->key.len = key.len;
@@ -447,15 +447,16 @@
 
         size_t new_bucket_count = (map->collection.size * 5) >> 1;
         if (new_bucket_count < hash_map->bucket_count) {
+            // LCOV_EXCL_START
             errno = EOVERFLOW;
             return 1;
-        }
+        } // LCOV_EXCL_STOP
         struct cx_hash_map_element_s **new_buckets = cxCalloc(
                 map->collection.allocator,
                 new_bucket_count, sizeof(struct cx_hash_map_element_s *)
         );
 
-        if (new_buckets == NULL) return 1;
+        if (new_buckets == NULL) return 1; // LCOV_EXCL_LINE
 
         // iterate through the elements and assign them to their new slots
         for (size_t slot = 0; slot < hash_map->bucket_count; slot++) {
--- a/ucx/json.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/json.c	Tue Dec 09 12:13:43 2025 +0100
@@ -94,7 +94,7 @@
     size_t newcap = obj->values_capacity;
     if (newcap > oldcap) {
         if (cxReallocateArray(al, &obj->indices, newcap, sizeof(size_t))) {
-            return 1;
+            return 1; // LCOV_EXCL_LINE
         }
     }
 
@@ -437,7 +437,7 @@
             } else if (c == 'u') {
                 char utf8buf[4];
                 unsigned utf8len = unescape_unicode_string(
-                    cx_strn(str.ptr + i - 1, str.length + 1 - i),
+                    cx_strn(str.ptr + i - 1, str.length - i),
                     utf8buf
                 );
                 if(utf8len > 0) {
@@ -456,7 +456,7 @@
             } else {
                 // TODO: discuss the behavior for unrecognized escape sequences
                 //       most parsers throw an error here - we just ignore it
-                result.ptr[result.length++] = '\\';
+                result.ptr[result.length++] = '\\'; // LCOV_EXCL_LINE
             }
 
             result.ptr[result.length++] = c;
@@ -640,6 +640,7 @@
     if (cxBufferEof(&json->buffer)) {
         // reinitialize the buffer
         cxBufferDestroy(&json->buffer);
+        if (buf == NULL) buf = ""; // buffer must not be initialized with NULL
         cxBufferInit(&json->buffer, (char*) buf, size,
             NULL, CX_BUFFER_AUTO_EXTEND | CX_BUFFER_COPY_ON_WRITE);
         json->buffer.size = size;
@@ -734,7 +735,8 @@
                     }
                 } else {
                     if (cx_strtod(token.content, &vbuf->value.number)) {
-                        return_rec(CX_JSON_FORMAT_ERROR_NUMBER);
+                        // TODO: at the moment this is unreachable, because the tokenizer is already stricter than cx_strtod()
+                        return_rec(CX_JSON_FORMAT_ERROR_NUMBER);  // LCOV_EXCL_LINE
                     }
                 }
                 return_rec(CX_JSON_NO_ERROR);
@@ -815,19 +817,19 @@
     } else {
         // should be unreachable
         assert(false);
-        return_rec(-1);
+        return_rec(-1); // LCOV_EXCL_LINE
     }
 }
 
 CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value) {
-    // check if buffer has been filled
+    // initialize output value
+    *value = &cx_json_value_nothing;
+
+    // check if the buffer has been filled
     if (json->buffer.space == NULL) {
         return CX_JSON_NULL_DATA;
     }
 
-    // initialize output value
-    *value = &cx_json_value_nothing;
-
     // parse data
     CxJsonStatus result;
     do {
@@ -943,11 +945,7 @@
     return v;
 }
 
-CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char* str) {
-    return cxJsonCreateCxString(allocator, cx_str(str));
-}
-
-CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str) {
+CxJsonValue* cx_json_create_string(const CxAllocator* allocator, cxstring str) {
     if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
@@ -1020,7 +1018,7 @@
     CxJsonValue** values = cxCallocDefault(count, sizeof(CxJsonValue*));
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
-        values[i] = cxJsonCreateCxString(arr->allocator, str[i]);
+        values[i] = cxJsonCreateString(arr->allocator, str[i]);
         if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
@@ -1050,61 +1048,56 @@
     );
 }
 
-int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
+int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
     cxmutstr k = cx_strdup_a(obj->allocator, name);
     if (k.ptr == NULL) return -1;
     CxJsonObjValue kv = {k, child};
     if (json_add_objvalue(obj, kv)) {
+        // LCOV_EXCL_START
         cx_strfree_a(obj->allocator, &k);
         return 1;
+        // LCOV_EXCL_STOP
     } else {
         return 0;
     }
 }
 
-CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name) {
+CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name) {
     CxJsonValue* v = cxJsonCreateObj(obj->allocator);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name) {
+CxJsonValue* cx_json_obj_put_arr(CxJsonValue* obj, cxstring name) {
     CxJsonValue* v = cxJsonCreateArr(obj->allocator);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num) {
+CxJsonValue* cx_json_obj_put_number(CxJsonValue* obj, cxstring name, double num) {
     CxJsonValue* v = cxJsonCreateNumber(obj->allocator, num);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num) {
+CxJsonValue* cx_json_obj_put_integer(CxJsonValue* obj, cxstring name, int64_t num) {
     CxJsonValue* v = cxJsonCreateInteger(obj->allocator, num);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str) {
+CxJsonValue* cx_json_obj_put_string(CxJsonValue* obj, cxstring name, cxstring str) {
     CxJsonValue* v = cxJsonCreateString(obj->allocator, str);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
     return v;
 }
 
-CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str) {
-    CxJsonValue* v = cxJsonCreateCxString(obj->allocator, str);
-    if (v == NULL) return NULL;
-    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
-    return v;
-}
-
-CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit) {
+CxJsonValue* cx_json_obj_put_literal(CxJsonValue* obj, cxstring name, CxJsonLiteral lit) {
     CxJsonValue* v = cxJsonCreateLiteral(obj->allocator, lit);
     if (v == NULL) return NULL;
     if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL;}
@@ -1286,9 +1279,6 @@
                                       ? look_idx
                                       : value->value.object.indices[look_idx];
                 CxJsonObjValue *member = &value->value.object.values[elem_idx];
-                if (settings->sort_members) {
-                    depth++;depth--;
-                }
 
                 // possible indentation
                 if (settings->pretty) {
@@ -1350,7 +1340,9 @@
                 if (cx_json_write_rec(
                         target, element,
                         wfunc, settings, depth)
-                ) return 1;
+                ) {
+                    return 1; // LCOV_EXCL_LINE
+                }
 
                 if (iter.index < iter.elem_count - 1) {
                     const char *arr_value_sep = ", ";
--- a/ucx/kv_list.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/kv_list.c	Tue Dec 09 12:13:43 2025 +0100
@@ -98,7 +98,7 @@
 }
 
 static CxHashKey *cx_kv_list_loc_key(cx_kv_list *list, void *node_data) {
-    return (CxHashKey*)((char*)node_data + list->list.base.collection.elem_size);
+    return (CxHashKey*)((char*)node_data - list->list.loc_data + list->list.loc_extra);
 }
 
 static void cx_kvl_deallocate(struct cx_list_s *list) {
@@ -559,7 +559,7 @@
     CxList *list = cxLinkedListCreate(allocator, comparator, elem_size);
     if (list == NULL) return NULL; // LCOV_EXCL_LINE
     cx_linked_list *ll = (cx_linked_list*)list;
-    ll->extra_data_len = sizeof(CxHashKey);
+    cx_linked_list_extra_data(ll, sizeof(CxHashKey));
     CxMap *map = cxHashMapCreate(allocator, CX_STORE_POINTERS, 0);
     if (map == NULL) { // LCOV_EXCL_START
         cxListFree(list);
--- a/ucx/linked_list.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/linked_list.c	Tue Dec 09 12:13:43 2025 +0100
@@ -31,6 +31,15 @@
 #include <string.h>
 #include <assert.h>
 
+#if __STDC_VERSION__ < 202311L
+// we cannot simply include stdalign.h
+// because Solaris is not entirely C11 complaint
+#ifndef __alignof_is_defined
+#define alignof _Alignof
+#define __alignof_is_defined 1
+#endif
+#endif
+
 // LOW LEVEL LINKED LIST FUNCTIONS
 
 #define CX_LL_PTR(cur, off) (*(void**)(((char*)(cur))+(off)))
@@ -516,7 +525,7 @@
     void *sbo[CX_LINKED_LIST_SORT_SBO_SIZE];
     void **sorted = length >= CX_LINKED_LIST_SORT_SBO_SIZE ?
                     cxMallocDefault(sizeof(void *) * length) : sbo;
-    if (sorted == NULL) abort();
+    if (sorted == NULL) abort(); // LCOV_EXCL_LINE
     void *rc, *lc;
 
     lc = ls;
@@ -692,8 +701,13 @@
 }
 
 static void *cx_ll_malloc_node(const cx_linked_list *list) {
-    return cxZalloc(list->base.collection.allocator,
-                    list->loc_data + list->base.collection.elem_size + list->extra_data_len);
+    size_t n;
+    if (list->extra_data_len == 0) {
+        n = list->loc_data + list->base.collection.elem_size;
+    } else {
+        n = list->loc_extra + list->extra_data_len;
+    }
+    return cxZalloc(list->base.collection.allocator, n);
 }
 
 static int cx_ll_insert_at(
@@ -1215,7 +1229,7 @@
         return result;
     } else {
         if (cx_ll_insert_element(list, list->collection.size, elem) == NULL) {
-            return 1;
+            return 1; // LCOV_EXCL_LINE
         }
         iter->elem_count++;
         iter->index = list->collection.size;
@@ -1267,12 +1281,22 @@
 
     cx_linked_list *list = cxCalloc(allocator, 1, sizeof(cx_linked_list));
     if (list == NULL) return NULL;
-    list->extra_data_len = 0;
     list->loc_prev = 0;
     list->loc_next = sizeof(void*);
     list->loc_data = sizeof(void*)*2;
+    list->loc_extra = -1;
+    list->extra_data_len = 0;
     cx_list_init((CxList*)list, &cx_linked_list_class,
             allocator, comparator, elem_size);
 
     return (CxList *) list;
 }
+
+void cx_linked_list_extra_data(cx_linked_list *list, size_t len) {
+    list->extra_data_len = len;
+
+    off_t loc_extra = list->loc_data + list->base.collection.elem_size;
+    size_t alignment = alignof(void*);
+    size_t padding = alignment - (loc_extra % alignment);
+    list->loc_extra = loc_extra + padding;
+}
--- a/ucx/list.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/list.c	Tue Dec 09 12:13:43 2025 +0100
@@ -1030,7 +1030,7 @@
         CxIterator src_iter = cxListIterator(src);
         CxIterator other_iter = cxListIterator(other);
         while (cxIteratorValid(src_iter) || cxIteratorValid(other_iter)) {
-            void *src_elem, *other_elem;
+            void *src_elem = NULL, *other_elem = NULL;
             int d;
             if (!cxIteratorValid(src_iter)) {
                 other_elem = cxIteratorCurrent(other_iter);
--- a/ucx/printf.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/printf.c	Tue Dec 09 12:13:43 2025 +0100
@@ -61,8 +61,10 @@
     va_copy(ap2, ap);
     int ret = vsnprintf(buf, CX_PRINTF_SBO_SIZE, fmt, ap);
     if (ret < 0) {
+        // LCOV_EXCL_START
         va_end(ap2);
         return ret;
+        // LCOV_EXCL_STOP
     } else if (ret < CX_PRINTF_SBO_SIZE) {
         va_end(ap2);
         return (int) wfc(buf, 1, ret, stream);
@@ -121,8 +123,10 @@
         if (s.ptr) {
             ret = vsnprintf(s.ptr, len, fmt, ap2);
             if (ret < 0) {
+                // LCOV_EXCL_START
                 cxFree(a, s.ptr);
                 s.ptr = NULL;
+                // LCOV_EXCL_STOP
             } else {
                 s.length = (size_t) ret;
             }
@@ -162,7 +166,7 @@
         if (ptr) {
             int newret = vsnprintf(ptr, newlen, fmt, ap2);
             if (newret < 0) {
-                cxFree(alloc, ptr);
+                cxFree(alloc, ptr); // LCOV_EXCL_LINE
             } else {
                 *len = newlen;
                 *str = ptr;
@@ -207,7 +211,7 @@
         if (ptr) {
             int newret = vsnprintf(ptr, newlen, fmt, ap2);
             if (newret < 0) {
-                cxFree(alloc, ptr);
+                cxFree(alloc, ptr); // LCOV_EXCL_LINE
             } else {
                 *len = newlen;
                 *str = ptr;
--- a/ucx/properties.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/properties.c	Tue Dec 09 12:13:43 2025 +0100
@@ -120,7 +120,7 @@
             // still not enough data, copy input buffer to internal buffer
             if (cxBufferAppend(input.ptr, 1,
                 input.length, &prop->buffer) < input.length) {
-                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
             // reset the input buffer (make way for a re-fill)
             cxBufferReset(&prop->input);
@@ -360,7 +360,7 @@
     // initialize reader
     if (source.read_init_func != NULL) {
         if (source.read_init_func(prop, &source)) {
-            return CX_PROPERTIES_READ_INIT_FAILED;
+            return CX_PROPERTIES_READ_INIT_FAILED;  // LCOV_EXCL_LINE
         }
     }
 
@@ -371,10 +371,10 @@
     while (true) {
         // read input
         cxstring input;
-        if (source.read_func(prop, &source, &input)) {
+        if (source.read_func(prop, &source, &input)) { // LCOV_EXCL_START
             status = CX_PROPERTIES_READ_FAILED;
             break;
-        }
+        } // LCOV_EXCL_STOP
 
         // no more data - break
         if (input.length == 0) {
@@ -401,7 +401,7 @@
             if (kv_status == CX_PROPERTIES_NO_ERROR) {
                 found = true;
                 if (sink.sink_func(prop, &sink, key, value)) {
-                    kv_status = CX_PROPERTIES_SINK_FAILED;
+                    kv_status = CX_PROPERTIES_SINK_FAILED;  // LCOV_EXCL_LINE
                 }
             }
         } while (kv_status == CX_PROPERTIES_NO_ERROR);
--- a/ucx/string.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/string.c	Tue Dec 09 12:13:43 2025 +0100
@@ -91,7 +91,7 @@
         cxstring src
 ) {
     if (cxReallocate(alloc, &dest->ptr, src.length + 1)) {
-        return 1;
+        return 1; // LCOV_EXCL_LINE
     }
 
     memcpy(dest->ptr, src.ptr, src.length);
@@ -137,7 +137,7 @@
     size_t slen = str.length;
     for (size_t i = 0; i < count; i++) {
         cxstring s = va_arg(ap, cxstring);
-        if (slen > SIZE_MAX - str.length) overflow = true;
+        if (slen > SIZE_MAX - s.length) overflow = true;
         slen += s.length;
     }
     va_end(ap);
@@ -156,10 +156,10 @@
     } else {
         newstr = cxRealloc(alloc, str.ptr, slen + 1);
     }
-    if (newstr == NULL) {
+    if (newstr == NULL) { // LCOV_EXCL_START
         va_end(ap2);
         return (cxmutstr) {NULL, 0};
-    }
+    } // LCOV_EXCL_STOP
     str.ptr = newstr;
 
     // concatenate strings
@@ -521,10 +521,12 @@
             cxMalloc(allocator, string.length + 1),
             string.length
     };
+    // LCOV_EXCL_START
     if (result.ptr == NULL) {
         result.length = 0;
         return result;
     }
+    // LCOV_EXCL_STOP
     memcpy(result.ptr, string.ptr, string.length);
     result.ptr[string.length] = '\0';
     return result;
--- a/ucx/tree.c	Wed Nov 12 18:37:58 2025 +0100
+++ b/ucx/tree.c	Tue Dec 09 12:13:43 2025 +0100
@@ -566,7 +566,7 @@
         ptrdiff_t loc_next
 ) {
     *cnode = cfunc(src, cdata);
-    if (*cnode == NULL) return 1;
+    if (*cnode == NULL) return 1;  // LCOV_EXCL_LINE
     cx_tree_zero_pointers(*cnode, cx_tree_ptr_locations);
 
     void *match = NULL;
@@ -627,7 +627,7 @@
 
         // create the new node
         void *new_node = cfunc(elem, cdata);
-        if (new_node == NULL) return processed;
+        if (new_node == NULL) return processed;  // LCOV_EXCL_LINE
         cx_tree_zero_pointers(new_node, cx_tree_ptr_locations);
 
         // start searching from current node
@@ -731,7 +731,7 @@
     void *node;
     if (tree->root == NULL) {
         node = tree->node_create(data, tree);
-        if (node == NULL) return 1;
+        if (node == NULL) return 1;  // LCOV_EXCL_LINE
         cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
         tree->root = node;
         tree->size = 1;
@@ -758,7 +758,7 @@
         // use the first element from the iter to create the root node
         void **eptr = iter->current(iter);
         void *node = tree->node_create(*eptr, tree);
-        if (node == NULL) return 0;
+        if (node == NULL) return 0;  // LCOV_EXCL_LINE
         cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
         tree->root = node;
         ins = 1;
@@ -780,7 +780,7 @@
         const void *data,
         size_t depth
 ) {
-    if (tree->root == NULL) return NULL;
+    if (tree->root == NULL) return NULL;  // LCOV_EXCL_LINE
 
     void *found;
     if (0 == cx_tree_search_data(
@@ -819,7 +819,7 @@
     assert(search_data_func != NULL);
 
     CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
-    if (tree == NULL) return NULL;
+    if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
 
     tree->cl = &cx_tree_default_class;
     tree->allocator = allocator;
@@ -857,7 +857,7 @@
     assert(root != NULL);
 
     CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
-    if (tree == NULL) return NULL;
+    if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
 
     tree->cl = &cx_tree_default_class;
     // set the allocator anyway, just in case...
@@ -893,7 +893,7 @@
 
 int cxTreeAddChild(CxTree *tree, void *parent, const void *data) {
     void *node = tree->node_create(data, tree);
-    if (node == NULL) return 1;
+    if (node == NULL) return 1; // LCOV_EXCL_LINE
     cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
     cx_tree_link(parent, node, cx_tree_node_layout(tree));
     tree->size++;
@@ -1071,6 +1071,7 @@
         cxFreeDefault(q);
         q = next;
     }
+    visitor->queue_next = visitor->queue_last = NULL;
 }
 
 CxTreeIterator cxTreeIterateSubtree(CxTree *tree, void *node, bool visit_on_exit) {

mercurial