tests/test_string.c

Mon, 29 Dec 2025 10:45:55 +0100

author
Mike Becker <universe@uap-core.de>
date
Mon, 29 Dec 2025 10:45:55 +0100
changeset 1678
6cf10bb137c5
parent 1677
1d73c7302fbc
child 1679
4a08dabe5e8f
permissions
-rw-r--r--

remove unnecessary explicit calls to cx_str() from the tests

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2023 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/test.h"
#include "util_allocator.h"

#include "cx/string.h"
#include "cx/compare.h"

#include <limits.h>
#include <errno.h>

#define ASSERT_ZERO_TERMINATED(str) CX_TEST_ASSERTM((str).ptr[(str).length] == '\0', \
    #str " is not zero terminated")

CX_TEST(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);
    cxstring snull = cx_str(NULL);
    cxmutstr mnull = cx_mutstr(NULL);
    CX_TEST_DO {
        CX_TEST_ASSERT(s1.length == 4);
        CX_TEST_ASSERT(strncmp(s1.ptr, "1234", 4) == 0);
        CX_TEST_ASSERT(s2.length == 2);
        CX_TEST_ASSERT(strncmp(s2.ptr, "ab", 2) == 0);
        CX_TEST_ASSERT(s3.length == 4);
        CX_TEST_ASSERT(strncmp(s3.ptr, "1234", 4) == 0);
        CX_TEST_ASSERT(s4.length == 2);
        CX_TEST_ASSERT(strncmp(s4.ptr, "ab", 2) == 0);
        CX_TEST_ASSERT(0 == snull.length);
        CX_TEST_ASSERT(NULL == snull.ptr);
        CX_TEST_ASSERT(0 == mnull.length);
        CX_TEST_ASSERT(NULL == mnull.ptr);
        CX_TEST_ASSERT(0 == cx_strcmp(snull, ""));
        CX_TEST_ASSERT(0 == cx_strcmp(mnull, ""));
    }
}

CX_TEST(test_strcast) {
    char *c1 = (char*) "123";
    const char *c2 = "abcde";
    unsigned char *c3 = (unsigned char*) "4711";
    unsigned const char *c4 = (unsigned const char*) "xyz0815";
    cxstring c5 = cx_str("foobar");
    cxmutstr c6 = cx_mutstr((char*)"hello world");
    cxstring s1 = cx_strcast(c1);
    cxstring s2 = cx_strcast(c2);
    cxstring s3 = cx_strcast(c3);
    cxstring s4 = cx_strcast(c4);
    cxstring s5 = cx_strcast(c5);
    cxstring s6 = cx_strcast(c6);
    CX_TEST_DO {
        CX_TEST_ASSERT(s1.length == 3);
        CX_TEST_ASSERT(strncmp(s1.ptr, "123", 3) == 0);
        CX_TEST_ASSERT(s2.length == 5);
        CX_TEST_ASSERT(strncmp(s2.ptr, "abcde", 5) == 0);
        CX_TEST_ASSERT(s3.length == 4);
        CX_TEST_ASSERT(strncmp(s3.ptr, "4711", 4) == 0);
        CX_TEST_ASSERT(s4.length == 7);
        CX_TEST_ASSERT(strncmp(s4.ptr, "xyz0815", 7) == 0);
        CX_TEST_ASSERT(s5.length == 6);
        CX_TEST_ASSERT(strncmp(s5.ptr, "foobar", 6) == 0);
        CX_TEST_ASSERT(s6.length == 11);
        CX_TEST_ASSERT(strncmp(s6.ptr, "hello world", 11) == 0);
    }
}

CX_TEST(test_strcast_m) {
    char *c1 = (char*) "123";
    const char *c2 = "abcde";
    unsigned char *c3 = (unsigned char*) "4711";
    unsigned const char *c4 = (unsigned const char*) "xyz0815";
    cxstring c5 = cx_str("foobar");
    cxmutstr c6 = cx_mutstr((char*)"hello world");
    cxmutstr s1 = cx_strcast_m(c1);
    cxstring s2 = cx_strcast_m(c2);
    cxmutstr s3 = cx_strcast_m(c3);
    cxstring s4 = cx_strcast_m(c4);
    cxstring s5 = cx_strcast_m(c5);
    cxmutstr s6 = cx_strcast_m(c6);
    CX_TEST_DO {
        CX_TEST_ASSERT(s1.length == 3);
        CX_TEST_ASSERT(strncmp(s1.ptr, "123", 3) == 0);
        CX_TEST_ASSERT(s2.length == 5);
        CX_TEST_ASSERT(strncmp(s2.ptr, "abcde", 5) == 0);
        CX_TEST_ASSERT(s3.length == 4);
        CX_TEST_ASSERT(strncmp(s3.ptr, "4711", 4) == 0);
        CX_TEST_ASSERT(s4.length == 7);
        CX_TEST_ASSERT(strncmp(s4.ptr, "xyz0815", 7) == 0);
        CX_TEST_ASSERT(s5.length == 6);
        CX_TEST_ASSERT(strncmp(s5.ptr, "foobar", 6) == 0);
        CX_TEST_ASSERT(s6.length == 11);
        CX_TEST_ASSERT(strncmp(s6.ptr, "hello world", 11) == 0);
    }
}

CX_TEST(test_strfree) {
    CxTestingAllocator talloc;
    cx_testing_allocator_init(&talloc);
    CxAllocator *alloc = &talloc.base;
    CX_TEST_DO {
        char *test = cxMalloc(alloc, 16);
        cxmutstr str = cx_mutstrn(test, 16);
        CX_TEST_ASSERT(str.ptr == test);
        CX_TEST_ASSERT(str.length == 16);
        cx_strfree_a(alloc, &str);
        CX_TEST_ASSERT(str.ptr == NULL);
        CX_TEST_ASSERT(str.length == 0);
        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
        // check that this does not explode
        cx_strfree(NULL);
        cx_strfree_a(alloc, NULL);
    }
    cx_testing_allocator_destroy(&talloc);
}

CX_TEST(test_strdup) {
    cxstring str = cx_str("test");
    cxmutstr dup = cx_strdup(str);
    CX_TEST_DO {
        CX_TEST_ASSERT(dup.length == str.length);
        CX_TEST_ASSERT(0 == strcmp(dup.ptr, str.ptr));
        ASSERT_ZERO_TERMINATED(dup);
    }
    cx_strfree(&dup);
}

CX_TEST(test_strdup_shortened) {
    cxstring str = cx_str("test");
    str.length = 2;
    cxmutstr dup = cx_strdup(str);
    CX_TEST_DO {
        CX_TEST_ASSERT(dup.length == str.length);
        CX_TEST_ASSERT(0 == strcmp(dup.ptr, "te"));
        ASSERT_ZERO_TERMINATED(dup);
    }
    cx_strfree(&dup);
}

CX_TEST(test_strcpy) {
    CxTestingAllocator talloc;
    cx_testing_allocator_init(&talloc);
    const CxAllocator *alloc = &talloc.base;
    cxstring str = cx_str("test string");
    str.length = 8; // test with a non-zero-terminated source
    cxmutstr dup;
    CX_TEST_DO {
        // copy into a smaller string
        dup = cx_strdup_a(alloc, "hello");
        CX_TEST_ASSERT(0 == cx_strcpy_a(alloc, &dup, str));
        CX_TEST_ASSERT(0 == cx_strcmp(dup, "test str"));
        ASSERT_ZERO_TERMINATED(dup);
        cx_strfree_a(alloc, &dup);

        // copy into a larger string
        dup = cx_strdup_a(alloc, "hello, world!");
        CX_TEST_ASSERT(0 == cx_strcpy_a(alloc, &dup, str));
        CX_TEST_ASSERT(0 == cx_strcmp(dup, "test str"));
        ASSERT_ZERO_TERMINATED(dup);
        cx_strfree_a(alloc, &dup);

        // copy into an equal-length string
        dup = cx_strdup_a(alloc, "testing!");
        CX_TEST_ASSERT(0 == cx_strcpy_a(alloc, &dup, str));
        CX_TEST_ASSERT(0 == cx_strcmp(dup, "test str"));
        ASSERT_ZERO_TERMINATED(dup);
        cx_strfree_a(alloc, &dup);

        // copy into a NULL-string
        dup.ptr = NULL;
        CX_TEST_ASSERT(0 == cx_strcpy_a(alloc, &dup, str));
        CX_TEST_ASSERT(0 == cx_strcmp(dup, "test str"));
        ASSERT_ZERO_TERMINATED(dup);
        cx_strfree_a(alloc, &dup);

        // copy a plain C-string
        CX_TEST_ASSERT(0 == cx_strcpy_a(alloc, &dup, "a c-string test"));
        CX_TEST_ASSERT(0 == cx_strcmp(dup, "a c-string test"));
        ASSERT_ZERO_TERMINATED(dup);
        cx_strfree_a(alloc, &dup);
    }
    cx_testing_allocator_destroy(&talloc);
}

CX_TEST(test_strlen) {
    cxstring s1 = cx_str("1234");
    cxstring s2 = cx_str(".:.:.");
    cxstring s3 = cx_str("X");
    CX_TEST_DO {
        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);

        CX_TEST_ASSERT(len0 == 0);
        CX_TEST_ASSERT(len1 == 4);
        CX_TEST_ASSERT(len2 == 9);
        CX_TEST_ASSERT(len3 == 10);
    }
}

CX_TEST(test_strsubs_cxs) {
    cxstring str = cx_str("A test string");
    cxstring sub;

    CX_TEST_DO {
        sub = cx_strsubs(str, 0);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, str));

        sub = cx_strsubs(str, 2);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test string"));

        sub = cx_strsubs(str, 7);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));

        sub = cx_strsubs(str, 15);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, ""));

        sub = cx_strsubsl(str, 2, 4);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test"));

        sub = cx_strsubsl(str, 7, 3);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "str"));

        sub = cx_strsubsl(str, 7, 20);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));
    }
}

CX_TEST(test_strsubs_cc) {
    const char *str = "A test string";
    cxstring sub;

    CX_TEST_DO {
        sub = cx_strsubs(str, 0);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, str));

        sub = cx_strsubs(str, 2);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test string"));

        sub = cx_strsubs(str, 7);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));

        sub = cx_strsubs(str, 15);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, ""));

        sub = cx_strsubsl(str, 2, 4);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test"));

        sub = cx_strsubsl(str, 7, 3);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "str"));

        sub = cx_strsubsl(str, 7, 20);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));
    }
}

CX_TEST(test_strsubs_cxms) {
    cxmutstr str = cx_mutstr((char*)"A test string");
    cxmutstr sub;

    CX_TEST_DO {
        sub = cx_strsubs(str, 0);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, str));

        sub = cx_strsubs(str, 2);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test string"));

        sub = cx_strsubs(str, 7);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));

        sub = cx_strsubs(str, 15);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, ""));

        sub = cx_strsubsl(str, 2, 4);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test"));

        sub = cx_strsubsl(str, 7, 3);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "str"));

        sub = cx_strsubsl(str, 7, 20);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));
    }
}

CX_TEST(test_strsubs_c) {
    char *str = "A test string";
    cxmutstr sub;

    CX_TEST_DO {
        sub = cx_strsubs(str, 0);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, str));

        sub = cx_strsubs(str, 2);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test string"));

        sub = cx_strsubs(str, 7);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));

        sub = cx_strsubs(str, 15);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, ""));

        sub = cx_strsubsl(str, 2, 4);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "test"));

        sub = cx_strsubsl(str, 7, 3);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "str"));

        sub = cx_strsubsl(str, 7, 20);
        CX_TEST_ASSERT(0 == cx_strcmp(sub, "string"));
    }
}

CX_TEST(test_strat) {
    cxstring str = cx_str("Hello, World!");

    CX_TEST_DO {
        // the entire string
        for (size_t i = 0; i < str.length; i++) {
            CX_TEST_ASSERT(cx_strat(str, i) == str.ptr[i]);
        }
        // the entire string backwards
        for (off_t i = 1; i <= (off_t) str.length; i++) {
            CX_TEST_ASSERT(cx_strat(str, -i) == str.ptr[str.length-i]);
        }
        // out of bounds (positive)
        CX_TEST_ASSERT(cx_strat(str, 12) == '!');
        CX_TEST_ASSERT(cx_strat(str, 13) == '\0');
        CX_TEST_ASSERT(cx_strat(str, 14) == '\0');
        CX_TEST_ASSERT(cx_strat(str, 651273) == '\0');
        // out of bounds (negative)
        CX_TEST_ASSERT(cx_strat(str, -13) == 'H');
        CX_TEST_ASSERT(cx_strat(str, -14) == '\0');
        CX_TEST_ASSERT(cx_strat(str, -15) == '\0');
        CX_TEST_ASSERT(cx_strat(str, -651273) == '\0');
    }
}

CX_TEST(test_strchr) {
    cxstring str = cx_str("I will find you - and I will kill you");

    CX_TEST_DO {
        cxstring notfound = cx_strchr(str, 'x');
        CX_TEST_ASSERT(notfound.length == 0);

        cxstring result = cx_strchr(str, 'w');
        CX_TEST_ASSERT(result.length == 35);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, "will find you - and I will kill you"));
    }
}

CX_TEST(test_strchr_m) {
    cxmutstr str = cx_mutstr((char*)"I will find you - and I will kill you");

    CX_TEST_DO {
        cxmutstr notfound = cx_strchr(str, 'x');
        CX_TEST_ASSERT(notfound.length == 0);

        cxmutstr result = cx_strchr(str, 'w');
        CX_TEST_ASSERT(result.length == 35);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, "will find you - and I will kill you"));
    }
}

CX_TEST(test_strrchr) {
    cxstring str = cx_str("X will find you - and I will kill you");

    CX_TEST_DO {
        cxstring notfound = cx_strrchr(str, 'x');
        CX_TEST_ASSERT(notfound.length == 0);

        cxstring result = cx_strrchr(str, 'w');
        CX_TEST_ASSERT(result.length == 13);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, "will kill you"));

        result = cx_strrchr(str, 'u');
        CX_TEST_ASSERT(result.length == 1);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, "u"));

        result = cx_strrchr(str, 'X');
        CX_TEST_ASSERT(0 == cx_strcmp(result, str));
    }
}

CX_TEST(test_strrchr_m) {
    cxmutstr str = cx_mutstr((char*)"X will find you - and I will kill you");

    CX_TEST_DO {
        cxmutstr notfound = cx_strrchr(str, 'x');
        CX_TEST_ASSERT(notfound.length == 0);

        cxmutstr result = cx_strrchr(str, 'w');
        CX_TEST_ASSERT(result.length == 13);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, "will kill you"));

        result = cx_strrchr(str, 'u');
        CX_TEST_ASSERT(result.length == 1);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, "u"));

        result = cx_strrchr(str, 'X');
        CX_TEST_ASSERT(0 == cx_strcmp(result, str));
    }
}

CX_TEST(test_strstr) {
    cxstring str = cx_str("find the match in this string");

    const size_t longstrpatternlen = 64 + cx_strstr_sbo_size;
    const size_t longstrlen = 320 + longstrpatternlen + 14;

    // it is more expensive to use calloc here, because we will overwrite
    // the memory anyway in the test preparation - but it is more reliable
    // in case we are doing something horribly wrong
    char *longstrc = calloc(longstrlen+1, 1);
    char *longstrpatternc = calloc(longstrpatternlen+1, 1);

    memcpy(longstrc,
           "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
           "mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx"
           "yzabcdeababababnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghij"
           "klmnopqrstuvwxyzaababababababababrstuvwxyzabcdefghijklmnopqrstuv"
           "abababababababababababababababababababababababababababababababab",
           320
    );
    memcpy(longstrpatternc,
           "abababababababababababababababababababababababababababababababab",
           64
    );
    char x = 'a', y='b', z;
    for (size_t i = 0; i < cx_strstr_sbo_size ; i++) {
        longstrpatternc[64+i] = x;
        longstrc[320+i] = x;
        z=x; x=y; y=z;
    }
    longstrpatternc[longstrpatternlen] = '\0';
    memcpy(longstrc+longstrlen-14, "wxyz1234567890", 15);

    cxmutstr longstr = cx_mutstrn(longstrc, longstrlen);
    cxstring longstrpattern = cx_strn(longstrpatternc, longstrpatternlen);
    cxmutstr longstrresult = cx_mutstrn(longstrc+256, longstrlen-256);

    CX_TEST_DO {
        cxstring notfound = cx_strstr(str, "no match");
        CX_TEST_ASSERT(notfound.length == 0);

        cxstring result = cx_strstr(str, "match");
        CX_TEST_ASSERT(result.length == 20);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, "match in this string"));

        result = cx_strstr(str, "");
        CX_TEST_ASSERT(result.length == str.length);
        CX_TEST_ASSERT(0 == strcmp(result.ptr, str.ptr));

        cxmutstr resultm = cx_strstr(longstr, longstrpattern);
        CX_TEST_ASSERT(resultm.length == longstrresult.length);
        CX_TEST_ASSERT(0 == strcmp(resultm.ptr, longstrresult.ptr));
    }

    free(longstrc);
    free(longstrpatternc);
}

CX_TEST(test_strcmp) {
    cxstring str = cx_str("compare this");
    CX_TEST_DO {
        CX_TEST_ASSERT(0 == cx_strcmp(cx_str(""), ""));
        CX_TEST_ASSERT(0 == cx_strcmp(CX_NULLSTR, ""));
        CX_TEST_ASSERT(0 < cx_strcmp(str, ""));
        CX_TEST_ASSERT(0 == cx_strcmp(str, "compare this"));
        CX_TEST_ASSERT(0 != cx_strcmp(str, "Compare This"));
        CX_TEST_ASSERT(0 > cx_strcmp(str, "compare tool"));
        CX_TEST_ASSERT(0 < cx_strcmp(str, "compare shit"));
        CX_TEST_ASSERT(0 > cx_strcmp(str, "compare this not"));
        CX_TEST_ASSERT(0 < cx_strcmp(str, "compare"));
        CX_TEST_ASSERT(0 > cx_strcmp(str, "lex"));
        CX_TEST_ASSERT(0 < cx_strcmp(str, "another lex test"));
        CX_TEST_ASSERT(0 < cx_strcmp(str, "Lex"));
        CX_TEST_ASSERT(0 < cx_strcmp(str, "Another lex test"));

        cxstring str2 = cx_str("Compare This");
        CX_TEST_ASSERT(0 != cx_strcmp_p(&str, &str2));
        str2 = cx_str("compare this");
        CX_TEST_ASSERT(0 == cx_strcmp_p(&str, &str2));
    }
}

CX_TEST(test_strcasecmp) {
    cxstring str = cx_str("compare this");
    CX_TEST_DO {
        CX_TEST_ASSERT(0 == cx_strcasecmp(cx_str(""), ""));
        CX_TEST_ASSERT(0 == cx_strcasecmp(CX_NULLSTR, ""));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, ""));
        CX_TEST_ASSERT(0 == cx_strcasecmp(str, "compare this"));
        CX_TEST_ASSERT(0 == cx_strcasecmp(str, "Compare This"));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, "compare tool"));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, "compare shit"));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, "compare this not"));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, "compare"));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, "lex"));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, "another lex test"));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, "Lex"));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, "Another lex test"));

        cxstring str2 = cx_str("Compare This");
        CX_TEST_ASSERT(0 == cx_strcasecmp_p(&str, &str2));
        str2 = cx_str("Compare Tool");
        CX_TEST_ASSERT(0 > cx_strcasecmp_p(&str, &str2));
    }
}

CX_TEST(test_strcat) {
    cxstring s1 = cx_str("12");
    cxstring s2 = cx_str("34");
    cxstring s3 = cx_str("56");
    cxstring sn = {NULL, 0};

    CxTestingAllocator talloc;
    cx_testing_allocator_init(&talloc);
    CxAllocator *alloc = &talloc.base;

    CX_TEST_DO {
        cxmutstr t1 = cx_strcat_a(alloc, CX_NULLSTR, 2, s1, s2);
        CX_TEST_ASSERT(0 == cx_strcmp(t1, "1234"));
        ASSERT_ZERO_TERMINATED(t1);
        cx_strfree_a(alloc, &t1);

        cxmutstr t2 = cx_strcat_a(alloc, CX_NULLSTR, 3, s1, s2, s3);
        CX_TEST_ASSERT(0 == cx_strcmp(t2, "123456"));
        ASSERT_ZERO_TERMINATED(t2);
        cx_strfree_a(alloc, &t2);

        cxmutstr t3 = cx_strcat_a(alloc, CX_NULLSTR, 6, s1, sn, s2, sn, s3, sn);
        CX_TEST_ASSERT(0 == cx_strcmp(t3, "123456"));
        ASSERT_ZERO_TERMINATED(t3);
        cx_strfree_a(alloc, &t3);

        cxmutstr t4 = cx_strcat_a(alloc, CX_NULLSTR, 2, sn, sn);
        CX_TEST_ASSERT(0 == cx_strcmp(t4, ""));
        ASSERT_ZERO_TERMINATED(t4);
        cx_strfree_a(alloc, &t4);

        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));

        // use the macro
        cxmutstr t5 = cx_strcat(CX_NULLSTR, 3, s3, s1, s2);
        CX_TEST_ASSERT(0 == cx_strcmp(t5, "561234"));
        ASSERT_ZERO_TERMINATED(t5);
        cx_strfree(&t5);

        // use an initial string
        cxmutstr t6 = cx_strdup("Hello");
        t6 = cx_strcat(t6, 2, cx_str(", "), cx_str("World!"));
        CX_TEST_ASSERT(0 == cx_strcmp(t6, "Hello, World!"));
        ASSERT_ZERO_TERMINATED(t6);
        cx_strfree(&t6);

        // test overflow with fake strings
        char *fakestr = NULL;
        cxstring a = cx_strn(fakestr, SIZE_MAX / 3 - 10);
        cxstring b = cx_strn(fakestr, SIZE_MAX / 3 - 5);
        cxstring c = cx_strn(fakestr, SIZE_MAX / 3 + 20);
        errno = 0;
        cxmutstr z = cx_strcat(CX_NULLSTR, 3, a, b, c);
        CX_TEST_ASSERT(errno == EOVERFLOW);
        CX_TEST_ASSERT(z.ptr == NULL);
        CX_TEST_ASSERT(z.length == 0);
    }
    cx_testing_allocator_destroy(&talloc);
}

CX_TEST(test_strcat_more_than_eight) {
    cxstring s1 = cx_str("12");
    cxstring s2 = cx_str("34");
    cxstring s3 = cx_str("56");
    cxstring s4 = cx_str("78");
    cxstring s5 = cx_str("9a");
    cxstring s6 = cx_str("bc");
    cxstring s7 = cx_str("de");
    cxstring s8 = cx_str("f0");
    cxstring s9 = cx_str("xy");

    CX_TEST_DO {
        cxmutstr r = cx_strcat(CX_NULLSTR, 9, s1, s2, s3, s4, s5, s6, s7, s8, s9);
        CX_TEST_ASSERT(0 == cx_strcmp(r, "123456789abcdef0xy"));
        ASSERT_ZERO_TERMINATED(r);
        cx_strfree(&r);
    }
}

CX_TEST(test_strsplit) {
    cxstring test = cx_str("this,is,a,csv,string");
    size_t capa = 8;
    cxstring list[8];
    size_t n;
    CX_TEST_DO {
        // special case: empty string
        n = cx_strsplit(test, "", capa, list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));

        // no delimiter occurrence
        n = cx_strsplit(test, "z", capa, list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));

        // partially matching delimiter
        n = cx_strsplit(test, "is,not", capa, list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));

        // matching single-char delimiter
        n = cx_strsplit(test, ",", capa, list);
        CX_TEST_ASSERT(n == 5);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "this"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], "is"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "a"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[3], "csv"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[4], "string"));

        // matching multi-char delimiter
        n = cx_strsplit(test, "is", capa, list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ","));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], ",a,csv,string"));

        // bounded list using single-char delimiter
        n = cx_strsplit(test, ",", 3, list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "this"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], "is"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "a,csv,string"));

        // bounded list using multi-char delimiter
        n = cx_strsplit(test, "is", 2, list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ",is,a,csv,string"));

        // start with delimiter
        n = cx_strsplit(test, "this", capa, list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ",is,a,csv,string"));

        // end with delimiter
        n = cx_strsplit(test, "string", capa, list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "this,is,a,csv,"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ""));


        // end with delimiter exceed bound
        n = cx_strsplit(cx_str("a,b,c,"), ",", 3, list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "a"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], "b"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "c,"));

        // exact match
        n = cx_strsplit(test, "this,is,a,csv,string", capa, list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ""));

        // string to be split is only substring
        n = cx_strsplit(test, "this,is,a,csv,string,with,extension", capa, list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));

        // subsequent encounter of delimiter (the string between is empty)
        n = cx_strsplit(test, "is,", capa, list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "a,csv,string"));

        // call the _m variant just for coverage
        cxmutstr mtest = cx_strdup(test);
        cxmutstr mlist[4];
        n = cx_strsplit_m(mtest, "is,", 4, mlist);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(mlist[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(mlist[1], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(mlist[2], "a,csv,string"));
        cx_strfree(&mtest);
    }
}

CX_TEST(test_strsplit_a) {
    CxTestingAllocator talloc;
    cx_testing_allocator_init(&talloc);
    CxAllocator *alloc = &talloc.base;

    cxstring test = cx_str("this,is,a,csv,string");
    size_t capa = 8;
    cxstring *list;
    size_t n;
    CX_TEST_DO {
        // special case: empty string
        n = cx_strsplit_a(alloc, test, "", capa, &list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));
        cxFree(alloc, list);

        // no delimiter occurrence
        n = cx_strsplit_a(alloc, test, "z", capa, &list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));
        cxFree(alloc, list);

        // partially matching delimiter
        n = cx_strsplit_a(alloc, test, "is,not", capa, &list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));
        cxFree(alloc, list);

        // matching single-char delimiter
        n = cx_strsplit_a(alloc, test, ",", capa, &list);
        CX_TEST_ASSERT(n == 5);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "this"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], "is"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "a"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[3], "csv"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[4], "string"));
        cxFree(alloc, list);

        // matching multi-char delimiter
        n = cx_strsplit_a(alloc, test, "is", capa, &list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ","));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], ",a,csv,string"));
        cxFree(alloc, list);

        // bounded list using single-char delimiter
        n = cx_strsplit_a(alloc, test, ",", 3, &list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "this"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], "is"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "a,csv,string"));
        cxFree(alloc, list);

        // bounded list using multi-char delimiter
        n = cx_strsplit_a(alloc, test, "is", 2, &list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ",is,a,csv,string"));
        cxFree(alloc, list);

        // start with delimiter
        n = cx_strsplit_a(alloc, test, "this", capa, &list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ",is,a,csv,string"));
        cxFree(alloc, list);

        // end with delimiter
        n = cx_strsplit_a(alloc, test, "string", capa, &list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "this,is,a,csv,"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ""));
        cxFree(alloc, list);

        // end with delimiter exceed bound
        n = cx_strsplit_a(alloc, cx_str("a,b,c,"), ",", 3, &list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "a"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], "b"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "c,"));
        cxFree(alloc, list);

        // exact match
        n = cx_strsplit_a(alloc, test, "this,is,a,csv,string", capa, &list);
        CX_TEST_ASSERT(n == 2);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ""));
        cxFree(alloc, list);

        // string to be split is only substring
        n = cx_strsplit_a(alloc, test, "this,is,a,csv,string,with,extension", capa, &list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));
        cxFree(alloc, list);

        // subsequent encounter of delimiter (the string between is empty)
        n = cx_strsplit_a(alloc, test, "is,", capa, &list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], "a,csv,string"));
        cxFree(alloc, list);

        // call the _m variant just for coverage
        cxmutstr mtest = cx_strdup(test);
        cxmutstr *mlist;
        n = cx_strsplit_ma(alloc, mtest, "is,", 4, &mlist);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(mlist[0], "th"));
        CX_TEST_ASSERT(0 == cx_strcmp(mlist[1], ""));
        CX_TEST_ASSERT(0 == cx_strcmp(mlist[2], "a,csv,string"));
        cxFree(alloc, mlist);
        cx_strfree(&mtest);

        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
    }
    cx_testing_allocator_destroy(&talloc);
}

CX_TEST(test_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(""));

    CX_TEST_DO {
        CX_TEST_ASSERT(0 == cx_strcmp(t1, cx_str("ein test")));
        CX_TEST_ASSERT(0 == cx_strcmp(t2, cx_str("abc")));
        CX_TEST_ASSERT(0 == cx_strcmp(t3, cx_str("123")));
        CX_TEST_ASSERT(0 == cx_strcmp(t4, cx_str("xyz")));
        CX_TEST_ASSERT(0 == cx_strcmp(t5, cx_str("")));
        CX_TEST_ASSERT(0 == cx_strcmp(empty, cx_str("")));

        // call the _m variant just for coverage
        cxmutstr m1 = cx_strtrim_m(cx_mutstr((char *) "  ein test  \t "));
        CX_TEST_ASSERT(0 == cx_strcmp(m1, "ein test"));
    }
}

CX_TEST(test_strprefix) {
    cxstring str = cx_str("test my prefix and my suffix");
    cxstring empty = cx_str("");
    CX_TEST_DO {
        CX_TEST_ASSERT(!cx_strprefix(empty, "pref"));
        CX_TEST_ASSERT(cx_strprefix(str, ""));
        CX_TEST_ASSERT(cx_strprefix(empty, ""));
        CX_TEST_ASSERT(cx_strprefix(str, "test "));
        CX_TEST_ASSERT(!cx_strprefix(str, "8-) fsck "));
    }
}

CX_TEST(test_strsuffix) {
    cxstring str = cx_str("test my prefix and my suffix");
    cxstring empty = cx_str("");
    CX_TEST_DO {
        CX_TEST_ASSERT(!cx_strsuffix(empty, "suf"));
        CX_TEST_ASSERT(cx_strsuffix(str, ""));
        CX_TEST_ASSERT(cx_strsuffix(empty, ""));
        CX_TEST_ASSERT(cx_strsuffix(str, "fix"));
        CX_TEST_ASSERT(!cx_strsuffix(str, "fox"));
    }
}

CX_TEST(test_strcaseprefix) {
    cxstring str = cx_str("test my prefix and my suffix");
    cxstring empty = cx_str("");
    CX_TEST_DO {
        CX_TEST_ASSERT(!cx_strcaseprefix(empty, "pREf"));
        CX_TEST_ASSERT(cx_strcaseprefix(str, ""));
        CX_TEST_ASSERT(cx_strcaseprefix(empty, ""));
        CX_TEST_ASSERT(cx_strcaseprefix(str, "TEST "));
        CX_TEST_ASSERT(!cx_strcaseprefix(str, "8-) fsck "));
    }
}

CX_TEST(test_strcasesuffix) {
    cxstring str = cx_str("test my prefix and my suffix");
    cxstring empty = cx_str("");
    CX_TEST_DO {
        CX_TEST_ASSERT(!cx_strcasesuffix(empty, "sUf"));
        CX_TEST_ASSERT(cx_strcasesuffix(str, ""));
        CX_TEST_ASSERT(cx_strcasesuffix(empty, ""));
        CX_TEST_ASSERT(cx_strcasesuffix(str, "FIX"));
        CX_TEST_ASSERT(!cx_strcasesuffix(str, "fox"));
    }
}

CX_TEST(test_strreplace) {
    CxTestingAllocator talloc;
    cx_testing_allocator_init(&talloc);
    CxAllocator *alloc = &talloc.base;

    cxstring str = cx_str("test ababab string aba");
    cxstring astr = cx_str("aaaaaaaaaa");
    cxstring csstr = cx_str("test AB ab TEST xyz");

    cxmutstr repl = cx_strreplace(str, "abab", "muchlonger");
    const char *expected = "test muchlongerab string aba";

    cxmutstr repln = cx_strreplacen(str, "ab", "c", 2);
    const char *expectedn = "test ccab string aba";

    cxmutstr longrepl = cx_strreplace("xyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacd", "a", "z");
    const char *longexpect = "xyzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzcd";

    cxmutstr replnotrail = cx_strreplace("test abab", "ab", "z");
    const char *notrailexpect = "test zz";

    cxmutstr repleq = cx_strreplace(str, str, "hello");
    const char *eqexpect = "hello";

    cxmutstr replempty1 = cx_strreplace("", "ab", "c"); // expect: empty
    cxmutstr replempty2 = cx_strreplace(str, "abab", "");
    const char *emptyexpect2 = "test ab string aba";

    cxmutstr replpre = cx_strreplace(str, "test ", "TEST ");
    const char *preexpected = "TEST ababab string aba";

    cxmutstr replan1 = cx_strreplacen(astr, "a", "x", 1);
    const char *an1expected = "xaaaaaaaaa";

    cxmutstr replan4 = cx_strreplacen(astr, "a", "x", 4);
    const char *an4expected = "xxxxaaaaaa";

    cxmutstr replan9 = cx_strreplacen(astr, "a", "x", 9);
    const char *an9expected = "xxxxxxxxxa";

    cxmutstr replan10 = cx_strreplacen(astr, "a", "x", 10);
    const char *an10expected = "xxxxxxxxxx";

    cxmutstr norepl = cx_strreplace(cx_strn("hello worlds", 11), "worlds", "test");
    const char *noreplexpect = "hello world";

    cxmutstr repl1_a = cx_strreplace_a(alloc, csstr, "AB", "*");
    const char *expeced1_a = "test * ab TEST xyz";

    cxmutstr repl2_a = cx_strreplace_a(alloc, csstr, "test", "TEST");
    const char *expected2_a = "TEST AB ab TEST xyz";

    CX_TEST_DO {
        CX_TEST_ASSERT(repl.ptr != str.ptr);
        ASSERT_ZERO_TERMINATED(repl);
        CX_TEST_ASSERT(0 == strcmp(repl.ptr, expected));
        ASSERT_ZERO_TERMINATED(repln);
        CX_TEST_ASSERT(0 == strcmp(repln.ptr, expectedn));
        ASSERT_ZERO_TERMINATED(longrepl);
        CX_TEST_ASSERT(0 == strcmp(longrepl.ptr, longexpect));
        ASSERT_ZERO_TERMINATED(replnotrail);
        CX_TEST_ASSERT(0 == strcmp(replnotrail.ptr, notrailexpect));
        ASSERT_ZERO_TERMINATED(repleq);
        CX_TEST_ASSERT(0 == strcmp(repleq.ptr, eqexpect));
        ASSERT_ZERO_TERMINATED(replempty1);
        CX_TEST_ASSERT(0 == strcmp(replempty1.ptr, ""));
        ASSERT_ZERO_TERMINATED(replempty2);
        CX_TEST_ASSERT(0 == strcmp(replempty2.ptr, emptyexpect2));
        ASSERT_ZERO_TERMINATED(replpre);
        CX_TEST_ASSERT(0 == strcmp(replpre.ptr, preexpected));
        ASSERT_ZERO_TERMINATED(replan1);
        CX_TEST_ASSERT(0 == strcmp(replan1.ptr, an1expected));
        ASSERT_ZERO_TERMINATED(replan4);
        CX_TEST_ASSERT(0 == strcmp(replan4.ptr, an4expected));
        ASSERT_ZERO_TERMINATED(replan9);
        CX_TEST_ASSERT(0 == strcmp(replan9.ptr, an9expected));
        ASSERT_ZERO_TERMINATED(replan10);
        CX_TEST_ASSERT(0 == strcmp(replan10.ptr, an10expected));
        ASSERT_ZERO_TERMINATED(repl1_a);
        CX_TEST_ASSERT(0 == strcmp(repl1_a.ptr, expeced1_a));
        ASSERT_ZERO_TERMINATED(repl2_a);
        CX_TEST_ASSERT(0 == strcmp(repl2_a.ptr, expected2_a));
        ASSERT_ZERO_TERMINATED(norepl);
        CX_TEST_ASSERT(0 == strcmp(norepl.ptr, noreplexpect));

        cx_strfree_a(alloc, &repl1_a);
        cx_strfree_a(alloc, &repl2_a);
        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
    }

    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(&norepl);
    cx_testing_allocator_destroy(&talloc);
}

CX_TEST(test_strtok) {
    cxstring str = cx_str("a,comma,separated,string");
    cxstring delim = cx_str(",");
    CX_TEST_DO {
        CxStrtokCtx ctx = cx_strtok(str, delim, 3);
        CX_TEST_ASSERT(ctx.str.ptr == str.ptr);
        CX_TEST_ASSERT(ctx.str.length == str.length);
        CX_TEST_ASSERT(ctx.delim.ptr == delim.ptr);
        CX_TEST_ASSERT(ctx.delim.length == delim.length);
        CX_TEST_ASSERT(ctx.limit == 3);
        CX_TEST_ASSERT(ctx.found == 0);
        CX_TEST_ASSERT(ctx.pos == 0);
        CX_TEST_ASSERT(ctx.next_pos == 0);
        CX_TEST_ASSERT(ctx.delim_more == NULL);
        CX_TEST_ASSERT(ctx.delim_more_count == 0);
    }
}

CX_TEST(test_strtok_delim) {
    cxstring str = cx_str("an,arbitrarily|separated;string");
    cxstring delim = cx_str(",");
    cxstring delim_more[2] = {cx_str("|"), cx_str(";")};
    CX_TEST_DO {
        CxStrtokCtx ctx = cx_strtok(str, delim, 3);
        cx_strtok_delim(&ctx, delim_more, 2);
        CX_TEST_ASSERT(ctx.str.ptr == str.ptr);
        CX_TEST_ASSERT(ctx.str.length == str.length);
        CX_TEST_ASSERT(ctx.delim.ptr == delim.ptr);
        CX_TEST_ASSERT(ctx.delim.length == delim.length);
        CX_TEST_ASSERT(ctx.limit == 3);
        CX_TEST_ASSERT(ctx.found == 0);
        CX_TEST_ASSERT(ctx.pos == 0);
        CX_TEST_ASSERT(ctx.next_pos == 0);
        CX_TEST_ASSERT(ctx.delim_more == delim_more);
        CX_TEST_ASSERT(ctx.delim_more_count == 2);
    }
}

CX_TEST(test_strtok_next_easy) {
    cxstring str = cx_str("a,comma,separated,string");
    cxstring delim = cx_str(",");
    CX_TEST_DO {
        CxStrtokCtx ctx = cx_strtok(str, delim, 3);
        bool ret;
        cxstring tok;

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "a"));
        CX_TEST_ASSERT(ctx.pos == 0);
        CX_TEST_ASSERT(ctx.next_pos == 2);
        CX_TEST_ASSERT(ctx.delim_pos == 1);
        CX_TEST_ASSERT(ctx.found == 1);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "comma"));
        CX_TEST_ASSERT(ctx.pos == 2);
        CX_TEST_ASSERT(ctx.next_pos == 8);
        CX_TEST_ASSERT(ctx.delim_pos == 7);
        CX_TEST_ASSERT(ctx.found == 2);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "separated"));
        CX_TEST_ASSERT(ctx.pos == 8);
        CX_TEST_ASSERT(ctx.next_pos == 18);
        CX_TEST_ASSERT(ctx.delim_pos == 17);
        CX_TEST_ASSERT(ctx.found == 3);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(!ret);
        CX_TEST_ASSERT(ctx.pos == 8);
        CX_TEST_ASSERT(ctx.next_pos == 18);
        CX_TEST_ASSERT(ctx.delim_pos == 17);
        CX_TEST_ASSERT(ctx.found == 3);
    }
}

CX_TEST(test_strtok_next_unlimited) {
    cxstring str = cx_str("some;-;otherwise;-;separated;-;string;-;");
    cxstring delim = cx_str(";-;");
    CX_TEST_DO {
        CxStrtokCtx ctx = cx_strtok(str, delim, SIZE_MAX);
        bool ret;
        cxstring tok;

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "some"));
        CX_TEST_ASSERT(ctx.pos == 0);
        CX_TEST_ASSERT(ctx.next_pos == 7);
        CX_TEST_ASSERT(ctx.delim_pos == 4);
        CX_TEST_ASSERT(ctx.found == 1);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "otherwise"));
        CX_TEST_ASSERT(ctx.pos == 7);
        CX_TEST_ASSERT(ctx.next_pos == 19);
        CX_TEST_ASSERT(ctx.delim_pos == 16);
        CX_TEST_ASSERT(ctx.found == 2);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "separated"));
        CX_TEST_ASSERT(ctx.pos == 19);
        CX_TEST_ASSERT(ctx.next_pos == 31);
        CX_TEST_ASSERT(ctx.delim_pos == 28);
        CX_TEST_ASSERT(ctx.found == 3);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "string"));
        CX_TEST_ASSERT(ctx.pos == 31);
        CX_TEST_ASSERT(ctx.next_pos == 40);
        CX_TEST_ASSERT(ctx.delim_pos == 37);
        CX_TEST_ASSERT(ctx.found == 4);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, ""));
        CX_TEST_ASSERT(ctx.pos == 40);
        CX_TEST_ASSERT(ctx.next_pos == 40);
        CX_TEST_ASSERT(ctx.delim_pos == 40);
        CX_TEST_ASSERT(ctx.found == 5);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(!ret);
        CX_TEST_ASSERT(ctx.pos == 40);
        CX_TEST_ASSERT(ctx.delim_pos == 40);
        CX_TEST_ASSERT(ctx.found == 5);
    }
}

static void test_toupper(cxmutstr string) {
    for (size_t i = 0; i < string.length; i++) {
        if ((unsigned int)(string.ptr[i] - 'a') < 26u) {
            string.ptr[i] += 'A' - 'a';
        }
    }
}

CX_TEST(test_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(";")};
    CX_TEST_DO {
        CxStrtokCtx ctx = cx_strtok(str, delim, 10);
        cx_strtok_delim(&ctx, delim_more, 2);
        bool ret;
        cxmutstr tok;

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "an"));
        CX_TEST_ASSERT(ctx.pos == 0);
        CX_TEST_ASSERT(ctx.next_pos == 3);
        CX_TEST_ASSERT(ctx.delim_pos == 2);
        CX_TEST_ASSERT(ctx.found == 1);
        test_toupper(tok);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "arbitrarily"));
        CX_TEST_ASSERT(ctx.pos == 3);
        CX_TEST_ASSERT(ctx.next_pos == 15);
        CX_TEST_ASSERT(ctx.delim_pos == 14);
        CX_TEST_ASSERT(ctx.found == 2);
        test_toupper(tok);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, ""));
        CX_TEST_ASSERT(ctx.pos == 15);
        CX_TEST_ASSERT(ctx.next_pos == 17);
        CX_TEST_ASSERT(ctx.delim_pos == 15);
        CX_TEST_ASSERT(ctx.found == 3);
        test_toupper(tok);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "separated"));
        CX_TEST_ASSERT(ctx.pos == 17);
        CX_TEST_ASSERT(ctx.next_pos == 27);
        CX_TEST_ASSERT(ctx.delim_pos == 26);
        CX_TEST_ASSERT(ctx.found == 4);
        test_toupper(tok);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(tok, "string"));
        CX_TEST_ASSERT(ctx.pos == 27);
        CX_TEST_ASSERT(ctx.next_pos == 33);
        CX_TEST_ASSERT(ctx.delim_pos == 33);
        CX_TEST_ASSERT(ctx.found == 5);
        test_toupper(tok);

        ret = cx_strtok_next(&ctx, &tok);
        CX_TEST_ASSERT(!ret);
        CX_TEST_ASSERT(ctx.pos == 27);
        CX_TEST_ASSERT(ctx.next_pos == 33);
        CX_TEST_ASSERT(ctx.delim_pos == 33);
        CX_TEST_ASSERT(ctx.found == 5);

        CX_TEST_ASSERT(0 == cx_strcmp(str, "AN,ARBITRARILY;||SEPARATED;STRING"));
    }
    cx_strfree(&str);
}

#define test_strtoint_impl(suffix, num, base, var, min, max) \
    do { \
        errno = 0; \
        int r = cx_strto##var(cx_str( #num), &var, base); \
        if ((min) <= (num##suffix) && (num##suffix) <= (max)) { \
            CX_TEST_ASSERTM(0 == r, "failed for "#num); \
            CX_TEST_ASSERT(0 == errno); \
            CX_TEST_ASSERT((num##suffix) == (0##suffix)+(var)); \
        } else { \
            CX_TEST_ASSERTM(0 != r, "out-of-range not detected for "#num " in variant "#var); \
            CX_TEST_ASSERT(ERANGE == errno); \
        } \
    } while (0)

#define test_strtoint_rollout_signed_impl(num, base) \
    test_strtoint_impl(LL, num, base, s, SHRT_MIN, SHRT_MAX); \
    test_strtoint_impl(LL, num, base, i, INT_MIN, INT_MAX); \
    test_strtoint_impl(LL, num, base, l, LONG_MIN, LONG_MAX); \
    test_strtoint_impl(LL, num, base, ll, LLONG_MIN, LLONG_MAX); \
    test_strtoint_impl(LL, num, base, i8, INT8_MIN, INT8_MAX); \
    test_strtoint_impl(LL, num, base, i16, INT16_MIN, INT16_MAX); \
    test_strtoint_impl(LL, num, base, i32, INT32_MIN, INT32_MAX); \
    test_strtoint_impl(LL, num, base, i64, INT64_MIN, INT64_MAX);

#define test_strtoint_rollout_signed(num, base) \
    test_strtoint_rollout_signed_impl(num, base); \
    test_strtoint_rollout_signed_impl(-num, base)

#define test_strtoint_rollout(num, base) \
    test_strtoint_impl(ULL, num, base, us, 0, USHRT_MAX); \
    test_strtoint_impl(ULL, num, base, u, 0, UINT_MAX); \
    test_strtoint_impl(ULL, num, base, ul, 0, ULONG_MAX); \
    test_strtoint_impl(ULL, num, base, ull, 0, ULLONG_MAX); \
    test_strtoint_impl(ULL, num, base, u8, 0, UINT8_MAX); \
    test_strtoint_impl(ULL, num, base, u16, 0, UINT16_MAX); \
    test_strtoint_impl(ULL, num, base, u32, 0, UINT32_MAX); \
    test_strtoint_impl(ULL, num, base, u64, 0, UINT64_MAX); \
    test_strtoint_impl(ULL, num, base, z, 0, SIZE_MAX)

CX_TEST(test_string_to_signed_integer) {
    short s;
    int i;
    long l;
    long long ll;
    int8_t i8;
    int16_t i16;
    int32_t i32;
    int64_t i64;
    CX_TEST_DO {
        // do some brute force tests with all ranges
        test_strtoint_rollout_signed(5, 10);
        test_strtoint_rollout_signed(47, 10);
        test_strtoint_rollout_signed(210, 10);
        test_strtoint_rollout_signed(5678, 10);
        test_strtoint_rollout_signed(40678, 10);
        test_strtoint_rollout_signed(1350266537, 10);
        test_strtoint_rollout_signed(3350266537, 10);
        test_strtoint_rollout_signed(473350266537, 10);
        test_strtoint_rollout_signed(057, 8);
        test_strtoint_rollout_signed(0322, 8);
        test_strtoint_rollout_signed(013056, 8);
        test_strtoint_rollout_signed(0117346, 8);
        test_strtoint_rollout_signed(012036667251, 8);
        test_strtoint_rollout_signed(030754201251, 8);
        test_strtoint_rollout_signed(06706567757251, 8);
        test_strtoint_rollout_signed(0767716340165362204025, 8);
        test_strtoint_rollout_signed(0x65, 16);
        test_strtoint_rollout_signed(0xf5, 16);
        test_strtoint_rollout_signed(0xABC5, 16);
        test_strtoint_rollout_signed(0xFBC5, 16);
        test_strtoint_rollout_signed(0x6df9CE03, 16);
        test_strtoint_rollout_signed(0xFdf9CE03, 16);
        test_strtoint_rollout_signed(0x6df9CE03AbC90815, 16);
#if __STDC_VERSION__ >= 202300L
        test_strtoint_rollout_signed(0b01000010100100101110101001110101, 2);
        test_strtoint_rollout_signed(0b00011010101100001111111001010100, 2);
        test_strtoint_rollout_signed(0b10110001001001010001010111010011, 2);
#endif

        // do some special case tests
        // --------------------------

        // can fit only in unsigned long long
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("0x8df9CE03AbC90815", &ll, 16));
        CX_TEST_ASSERT(errno == ERANGE);

        // negative overflow (we only test for 64 bit long long)
#if LLONG_MAX == 9223372036854775807ll
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtoll("-9223372036854775808", &ll, 10));
        CX_TEST_ASSERT(ll == LLONG_MIN);
        CX_TEST_ASSERT(errno == 0);
        CX_TEST_ASSERT(0 != cx_strtoll("-9223372036854775809", &ll, 10));
        CX_TEST_ASSERT(errno == ERANGE);
#endif

        // edge case: empty and NULL string
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("", &ll, 16));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll(CX_NULLSTR, &ll, 16));
        CX_TEST_ASSERT(errno == EINVAL);

        // edge case: unsupported base
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("7", &ll, 12));
        CX_TEST_ASSERT(errno == EINVAL);

        // edge case: incorrect sign characters
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("-", &ll, 10));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("+", &ll, 10));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("-+15", &ll, 10));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("--15", &ll, 10));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("+-15", &ll, 10));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoll("++15", &ll, 10));
        CX_TEST_ASSERT(errno == EINVAL);

        // edge case: only the sign bit is set
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi16("0x8000", &i16, 16));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtoi16("-0x8000", &i16, 16));
        CX_TEST_ASSERT(errno == 0);
        CX_TEST_ASSERT(i16 == INT16_MIN);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi64("X8000000000000000", &i64, 16));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtoi64("-X8000000000000000", &i64, 16));
        CX_TEST_ASSERT(errno == 0);
        CX_TEST_ASSERT(i64 == INT64_MIN);

        // group separators
        CX_TEST_ASSERT(0 == cx_strtoi32("-123,456", &i32, 10));
        CX_TEST_ASSERT(i32 == -123456);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi16_lc("-Xab,cd", &i16, 16, "'"));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi16_lc("-X'ab'cd", &i16, 16, "'"));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtoi16_lc("-X'67'89", &i16, 16, "'"));
        CX_TEST_ASSERT(errno == 0);
        CX_TEST_ASSERT(i16 == -0x6789);

        // binary and (unusual notation of) signed binary
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi8_lc("-1010 1011", &i8, 2, " "));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi8_lc("1010 1011", &i8, 2, " "));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtoi8_lc("-0101 0101", &i8, 2, " "));
        CX_TEST_ASSERT(errno == 0);
        CX_TEST_ASSERT(i8 == -0x55);
    }
}

CX_TEST(test_string_to_unsigned_integer) {
    unsigned short us;
    unsigned int u;
    unsigned long ul;
    unsigned long long ull;
    uint8_t u8;
    uint16_t u16;
    uint32_t u32;
    uint64_t u64;
    size_t z;
    CX_TEST_DO {
        // do some brute force tests with all ranges
        test_strtoint_rollout(0, 10);
        test_strtoint_rollout(47, 10);
        test_strtoint_rollout(210, 10);
        test_strtoint_rollout(5678, 10);
        test_strtoint_rollout(40678, 10);
        test_strtoint_rollout(1350266537, 10);
        test_strtoint_rollout(3350266537, 10);
        test_strtoint_rollout(473350266537, 10);
        test_strtoint_rollout(0, 8);
        test_strtoint_rollout(057, 8);
        test_strtoint_rollout(0322, 8);
        test_strtoint_rollout(013056, 8);
        test_strtoint_rollout(0117346, 8);
        test_strtoint_rollout(012036667251, 8);
        test_strtoint_rollout(030754201251, 8);
        test_strtoint_rollout(06706567757251, 8);
        test_strtoint_rollout(01767716340165362204025, 8);
        test_strtoint_rollout(0x0, 16);
        test_strtoint_rollout(0, 16);
        test_strtoint_rollout(0x65, 16);
        test_strtoint_rollout(0xf5, 16);
        test_strtoint_rollout(0xABC5, 16);
        test_strtoint_rollout(0xFBC5, 16);
        test_strtoint_rollout(0x6df9CE03, 16);
        test_strtoint_rollout(0xFdf9CE03, 16);
        test_strtoint_rollout(0x6df9CE03AbC90815, 16);
        test_strtoint_rollout(0xfdf9CE03AbC90815, 16);
#if __STDC_VERSION__ >= 202300L
        test_strtoint_rollout(0b0, 2);
        test_strtoint_rollout(0, 2);
        test_strtoint_rollout(0b01000010100100101110101001110101, 2);
        test_strtoint_rollout(0b00011010101100001111111001010100, 2);
        test_strtoint_rollout(0b10110001001001010001010111010011, 2);
#endif

        // do some special case tests
        // --------------------------

        // leading plus
        CX_TEST_ASSERT(0 == cx_strtou32("+5", &u32, 10));
        CX_TEST_ASSERT(u32 == 5);

        // group separators
        CX_TEST_ASSERT(0 == cx_strtou32("123,456", &u32, 10));
        CX_TEST_ASSERT(u32 == 123456);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtou16_lc("ab,cd", &u16, 16, "'"));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtou16_lc("ab'cd", &u16, 16, "'"));
        CX_TEST_ASSERT(errno == 0);
        CX_TEST_ASSERT(u16 == 0xabcd);

        // binary
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtou8_lc("1 1010 1011", &u8, 2, " "));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtou8_lc("1010 1011", &u8, 2, " "));
        CX_TEST_ASSERT(errno == 0);
        CX_TEST_ASSERT(u8 == 0xAB);

        // edge case: empty and NULL string
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull("", &ull, 16));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull(CX_NULLSTR, &ull, 16));
        CX_TEST_ASSERT(errno == EINVAL);

        // edge case: unsupported base
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull("7", &ull, 12));
        CX_TEST_ASSERT(errno == EINVAL);

        // edge case: prefix only
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull("0b", &ull, 2));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull("b", &ull, 2));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull("0x", &ull, 16));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull("x", &ull, 16));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoull("#", &ull, 16));
        CX_TEST_ASSERT(errno == EINVAL);
    }
}

CX_TEST(test_string_to_float) {
    float f;
    CX_TEST_DO {
        CX_TEST_ASSERT(0 == cx_strtof("11.3", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(11.3f, f));

        CX_TEST_ASSERT(0 == cx_strtof("-4.711e+1", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(-47.11f, f));

        CX_TEST_ASSERT(0 == cx_strtof("1.67262192595e-27", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(1.67262192595e-27f, f));

        CX_TEST_ASSERT(0 == cx_strtof_lc("138,339.4", &f, '.', ","));
        CX_TEST_ASSERT(0 == cx_vcmp_float(138339.4f, f));

        CX_TEST_ASSERT(0 == cx_strtof_lc("138,339.4", &f, ',', "."));
        CX_TEST_ASSERT(0 == cx_vcmp_float(138.3394f, f));

        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof("15e", &f));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof("15e+", &f));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof("15e-", &f));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(0 == cx_strtof("15e-0", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(15.f, f));

        CX_TEST_ASSERT(0 == cx_strtof("3e38", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(3e38f, f));
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof("3e39", &f));
        CX_TEST_ASSERT(errno == ERANGE);
        CX_TEST_ASSERT(0 == cx_strtof("-3e38", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(-3e38f, f));
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof("-3e39", &f));
        CX_TEST_ASSERT(errno == ERANGE);
        CX_TEST_ASSERT(0 == cx_strtof("1.18e-38", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(1.18e-38f, f));
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof("1.17e-38", &f));
        CX_TEST_ASSERT(errno == ERANGE);
    }
}

CX_TEST(test_string_to_double) {
    double d;
    CX_TEST_DO {
        CX_TEST_ASSERT(0 == cx_strtod("11.3", &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(11.3, d));

        CX_TEST_ASSERT(0 == cx_strtod("13.", &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(13.0, d));

        CX_TEST_ASSERT(0 == cx_strtod("47", &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(47.0, d));

        CX_TEST_ASSERT(0 == cx_strtod("-13.37", &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(-13.37, d));

        CX_TEST_ASSERT(0 == cx_strtod("-4.711e+1", &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(-47.11, d));

        CX_TEST_ASSERT(0 == cx_strtod("1.67262192595e-27", &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(1.67262192595e-27, d));

        CX_TEST_ASSERT(0 == cx_strtod_lc("138,339.4", &d, '.', ","));
        CX_TEST_ASSERT(0 == cx_vcmp_double(138339.4, d));

        CX_TEST_ASSERT(0 == cx_strtod_lc("138,339.4", &d, ',', "."));
        CX_TEST_ASSERT(0 == cx_vcmp_double(138.3394, d));

        CX_TEST_ASSERT(0 == cx_strtod_lc("13.37e04.7", &d, ',', "."));
        CX_TEST_ASSERT(0 == cx_vcmp_double(1337e47, d));

        d = 47.11;
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod(CX_NULLSTR, &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("+", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("-", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("+-5", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("-+5", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("++5", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("--5", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);

        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod_lc(".", &d, '.', "'"));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);

        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtod("19e0x5", &d));
        CX_TEST_ASSERT(errno == EINVAL);
        CX_TEST_ASSERT(d == 47.11);

        // TODO: test and improve support for big numbers, precision, and out-of-range detection
    }
}

CX_TEST(test_string_to_number_notrim) {
    long long i;
    unsigned long long u;
    float f;
    double d;
    CX_TEST_DO {
        CX_TEST_ASSERT(0 != cx_strtoll("-42 ", &i, 10));
        CX_TEST_ASSERT(0 != cx_strtoll(" -42", &i, 10));
        CX_TEST_ASSERT(0 == cx_strtoll("-42", &i, 10));
        CX_TEST_ASSERT(i == -42);

        CX_TEST_ASSERT(0 != cx_strtoull("42 ", &u, 10));
        CX_TEST_ASSERT(0 != cx_strtoull(" 42", &u, 10));
        CX_TEST_ASSERT(0 == cx_strtoull("42", &u, 10));
        CX_TEST_ASSERT(u == 42);

        CX_TEST_ASSERT(0 != cx_strtof("13.37 ", &f));
        CX_TEST_ASSERT(0 != cx_strtof(" 13.37", &f));
        CX_TEST_ASSERT(0 == cx_strtof("13.37", &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(f, 13.37f));

        CX_TEST_ASSERT(0 != cx_strtod("13.37 ", &d));
        CX_TEST_ASSERT(0 != cx_strtod(" 13.37", &d));
        CX_TEST_ASSERT(0 == cx_strtod("13.37", &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(d, 13.37));
    }
}

CX_TEST(test_strformat) {
    cxstring str = cx_str("Hello, World!");
    CX_TEST_DO {
        char actual[64];
        snprintf(actual, 64, "Test %" CX_PRIstr " Success.", CX_SFMT(str));
        CX_TEST_ASSERT(0 == strncmp("Test Hello, World! Success.", actual, 64));
    }
}

CxTestSuite *cx_test_suite_string(void) {
    CxTestSuite *suite = cx_test_suite_new("string");

    cx_test_register(suite, test_string_construct);
    cx_test_register(suite, test_strcast);
    cx_test_register(suite, test_strcast_m);
    cx_test_register(suite, test_strfree);
    cx_test_register(suite, test_strdup);
    cx_test_register(suite, test_strdup_shortened);
    cx_test_register(suite, test_strcpy);
    cx_test_register(suite, test_strlen);
    cx_test_register(suite, test_strsubs_cxs);
    cx_test_register(suite, test_strsubs_cxms);
    cx_test_register(suite, test_strsubs_cc);
    cx_test_register(suite, test_strsubs_c);
    cx_test_register(suite, test_strat);
    cx_test_register(suite, test_strchr);
    cx_test_register(suite, test_strchr_m);
    cx_test_register(suite, test_strrchr);
    cx_test_register(suite, test_strrchr_m);
    cx_test_register(suite, test_strstr);
    cx_test_register(suite, test_strcmp);
    cx_test_register(suite, test_strcasecmp);
    cx_test_register(suite, test_strcat);
    cx_test_register(suite, test_strcat_more_than_eight);
    cx_test_register(suite, test_strsplit);
    cx_test_register(suite, test_strsplit_a);
    cx_test_register(suite, test_strtrim);
    cx_test_register(suite, test_strprefix);
    cx_test_register(suite, test_strsuffix);
    cx_test_register(suite, test_strcaseprefix);
    cx_test_register(suite, test_strcasesuffix);
    cx_test_register(suite, test_strreplace);
    cx_test_register(suite, test_strtok);
    cx_test_register(suite, test_strtok_delim);
    cx_test_register(suite, test_strtok_next_easy);
    cx_test_register(suite, test_strtok_next_unlimited);
    cx_test_register(suite, test_strtok_next_advanced);
    cx_test_register(suite, test_strformat);

    return suite;
}

CxTestSuite *cx_test_suite_string_to_number(void) {
    CxTestSuite *suite = cx_test_suite_new("string to number");

    cx_test_register(suite, test_string_to_signed_integer);
    cx_test_register(suite, test_string_to_unsigned_integer);
    cx_test_register(suite, test_string_to_float);
    cx_test_register(suite, test_string_to_double);
    cx_test_register(suite, test_string_to_number_notrim);

    return suite;
}

mercurial