tests/test_string.c

Fri, 23 May 2025 12:44:24 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 23 May 2025 12:44:24 +0200
changeset 1327
ed75dc1db503
parent 1304
57e062a4bb05
permissions
-rw-r--r--

make test-compile depend on both static and shared

the shared lib is not needed for the tests,
but when run with coverage, gcov will be confused
when outdated line information is available from
a previous shared build

/*
 * 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);
    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(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));
    }
    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, CX_STR("hello"));
        CX_TEST_ASSERT(0 == cx_strcpy_a(alloc, &dup, str));
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(dup), CX_STR("test str")));
        ASSERT_ZERO_TERMINATED(dup);
        cx_strfree_a(alloc, &dup);

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

        // copy into an equal-length string
        dup = cx_strdup_a(alloc, CX_STR("testing!"));
        CX_TEST_ASSERT(0 == cx_strcpy_a(alloc, &dup, str));
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(dup), CX_STR("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(cx_strcast(dup), CX_STR("test str")));
        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) {
    cxstring str = CX_STR("A test string");

    CX_TEST_DO {
        cxstring 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, cx_str("test string")));

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

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

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

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

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

        // just for coverage, call the _m variant
        cxmutstr m = cx_strsubs_m(cx_mutstrn(NULL, 0), 0);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(m), cx_str("")));
    }
}

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"));

        // just for coverage, call the _m variant
        cxmutstr m = cx_strchr_m(cx_mutstrn(NULL, 0), 'a');
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(m), cx_str("")));
    }
}

CX_TEST(test_strrchr) {
    cxstring str = CX_STR("I 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"));

        // just for coverage, call the _m variant
        cxmutstr m = cx_strrchr_m(cx_mutstrn(NULL, 0), 'a');
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(m), cx_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, cx_str("no match"));
        CX_TEST_ASSERT(notfound.length == 0);

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

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

        cxmutstr resultm = cx_strstr_m(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_str("")));
        CX_TEST_ASSERT(0 < cx_strcmp(str, cx_str("")));
        CX_TEST_ASSERT(0 == cx_strcmp(str, cx_str("compare this")));
        CX_TEST_ASSERT(0 != cx_strcmp(str, cx_str("Compare This")));
        CX_TEST_ASSERT(0 > cx_strcmp(str, cx_str("compare tool")));
        CX_TEST_ASSERT(0 < cx_strcmp(str, cx_str("compare shit")));
        CX_TEST_ASSERT(0 > cx_strcmp(str, cx_str("compare this not")));
        CX_TEST_ASSERT(0 < cx_strcmp(str, cx_str("compare")));
        CX_TEST_ASSERT(0 > cx_strcmp(str, cx_str("lex")));
        CX_TEST_ASSERT(0 < cx_strcmp(str, cx_str("another lex test")));
        CX_TEST_ASSERT(0 < cx_strcmp(str, cx_str("Lex")));
        CX_TEST_ASSERT(0 < cx_strcmp(str, cx_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_str("")));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, cx_str("")));
        CX_TEST_ASSERT(0 == cx_strcasecmp(str, cx_str("compare this")));
        CX_TEST_ASSERT(0 == cx_strcasecmp(str, cx_str("Compare This")));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, cx_str("compare tool")));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, cx_str("compare shit")));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, cx_str("compare this not")));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, cx_str("compare")));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, cx_str("lex")));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, cx_str("another lex test")));
        CX_TEST_ASSERT(0 > cx_strcasecmp(str, cx_str("Lex")));
        CX_TEST_ASSERT(0 < cx_strcasecmp(str, cx_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, 2, s1, s2);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(t1), cx_str("1234")));
        ASSERT_ZERO_TERMINATED(t1);
        cx_strfree_a(alloc, &t1);

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

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

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

        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));

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

        // use an initial string
        cxmutstr t6 = cx_strdup(cx_str("Hello"));
        t6 = cx_strcat_m(t6, 2, cx_str(", "), cx_str("World!"));
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(t6), cx_str("Hello, World!")));
        ASSERT_ZERO_TERMINATED(t6);
        cx_strfree(&t6);
    }
    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(9, s1, s2, s3, s4, s5, s6, s7, s8, s9);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(r), cx_str("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, cx_str(""), capa, list);
        CX_TEST_ASSERT(n == 1);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], test));

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

        // partially matching delimiter
        n = cx_strsplit(test, cx_str("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, cx_str(","), capa, list);
        CX_TEST_ASSERT(n == 5);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], cx_str("this")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], cx_str("is")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], cx_str("a")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[3], cx_str("csv")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[4], cx_str("string")));

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

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

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

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

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


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

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

        // string to be split is only substring
        n = cx_strsplit(test, cx_str("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, cx_str("is,"), capa, list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], cx_str("th")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], cx_str("")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], cx_str("a,csv,string")));

        // call the _m variant just for coverage
        cxmutstr mtest = cx_strdup(test);
        cxmutstr mlist[4];
        n = cx_strsplit_m(mtest, cx_str("is,"), 4, mlist);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(mlist[0]), cx_str("th")));
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(mlist[1]), cx_str("")));
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(mlist[2]), cx_str("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, cx_str(""), 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, cx_str("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, cx_str("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, cx_str(","), capa, &list);
        CX_TEST_ASSERT(n == 5);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], cx_str("this")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], cx_str("is")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], cx_str("a")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[3], cx_str("csv")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[4], cx_str("string")));
        cxFree(alloc, list);

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

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

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

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

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

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

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

        // string to be split is only substring
        n = cx_strsplit_a(alloc, test, cx_str("this,is,a,csv,string,with,extension"), capa, &list);
        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, cx_str("is,"), capa, &list);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(list[0], cx_str("th")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[1], cx_str("")));
        CX_TEST_ASSERT(0 == cx_strcmp(list[2], cx_str("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, cx_str("is,"), 4, &mlist);
        CX_TEST_ASSERT(n == 3);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(mlist[0]), cx_str("th")));
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(mlist[1]), cx_str("")));
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(mlist[2]), cx_str("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(cx_strcast(m1), cx_str("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, cx_str("pref")));
        CX_TEST_ASSERT(cx_strprefix(str, empty));
        CX_TEST_ASSERT(cx_strprefix(empty, empty));
        CX_TEST_ASSERT(cx_strprefix(str, cx_str("test ")));
        CX_TEST_ASSERT(!cx_strprefix(str, cx_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, cx_str("suf")));
        CX_TEST_ASSERT(cx_strsuffix(str, empty));
        CX_TEST_ASSERT(cx_strsuffix(empty, empty));
        CX_TEST_ASSERT(cx_strsuffix(str, cx_str("fix")));
        CX_TEST_ASSERT(!cx_strsuffix(str, cx_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, cx_str("pREf")));
        CX_TEST_ASSERT(cx_strcaseprefix(str, empty));
        CX_TEST_ASSERT(cx_strcaseprefix(empty, empty));
        CX_TEST_ASSERT(cx_strcaseprefix(str, cx_str("TEST ")));
        CX_TEST_ASSERT(!cx_strcaseprefix(str, cx_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, cx_str("sUf")));
        CX_TEST_ASSERT(cx_strcasesuffix(str, empty));
        CX_TEST_ASSERT(cx_strcasesuffix(empty, empty));
        CX_TEST_ASSERT(cx_strcasesuffix(str, cx_str("FIX")));
        CX_TEST_ASSERT(!cx_strcasesuffix(str, cx_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 longstr = CX_STR(
            "xyaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacd");
    cxstring notrail = CX_STR("test abab");
    cxstring empty = CX_STR("");
    cxstring astr = CX_STR("aaaaaaaaaa");
    cxstring csstr = CX_STR("test AB ab TEST xyz");

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

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

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

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

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

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

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

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

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

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

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

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

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

        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));

        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_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, cx_str("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, cx_str("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, cx_str("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, cx_str("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, cx_str("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, cx_str("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, cx_str("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_str("")));
        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_m(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(tok), cx_str("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_m(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(tok), cx_str("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_m(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(tok), cx_str("")));
        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_m(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(tok), cx_str("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_m(&ctx, &tok);
        CX_TEST_ASSERT(ret);
        CX_TEST_ASSERT(0 == cx_strcmp(cx_strcast(tok), cx_str("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_m(&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(cx_strcast(str), cx_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);
        // TODO: roll out base 2 tests, but that needs C23

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

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

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

        // group separators
        CX_TEST_ASSERT(0 == cx_strtoi32(cx_str("-123,456"), &i32, 10));
        CX_TEST_ASSERT(i32 == -123456);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi16_lc(cx_str("-Xab,cd"), &i16, 16, "'"));
        CX_TEST_ASSERT(errno == EINVAL);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi16_lc(cx_str("-X'ab'cd"), &i16, 16, "'"));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtoi16_lc(cx_str("-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(cx_str("-1010 1011"), &i8, 2, " "));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtoi8_lc(cx_str("1010 1011"), &i8, 2, " "));
        CX_TEST_ASSERT(errno == ERANGE);
        errno = 0;
        CX_TEST_ASSERT(0 == cx_strtoi8_lc(cx_str("-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(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(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(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);
        // TODO: roll out base 2 tests, but that needs C23

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

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

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

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

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

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

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

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

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

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

        CX_TEST_ASSERT(0 == cx_strtof(cx_str("3e38"), &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(3e38f, f));
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof(cx_str("3e39"), &f));
        CX_TEST_ASSERT(errno == ERANGE);
        CX_TEST_ASSERT(0 == cx_strtof(cx_str("-3e38"), &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(-3e38f, f));
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof(cx_str("-3e39"), &f));
        CX_TEST_ASSERT(errno == ERANGE);
        CX_TEST_ASSERT(0 == cx_strtof(cx_str("1.18e-38"), &f));
        CX_TEST_ASSERT(0 == cx_vcmp_float(1.18e-38f, f));
        errno = 0;
        CX_TEST_ASSERT(0 != cx_strtof(cx_str("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(cx_str("11.3"), &d));
        CX_TEST_ASSERT(0 == cx_vcmp_double(11.3, d));

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

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

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

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

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

        // 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(cx_str("-42 "), &i, 10));
        CX_TEST_ASSERT(0 != cx_strtoll(cx_str(" -42"), &i, 10));
        CX_TEST_ASSERT(0 == cx_strtoll(cx_str("-42"), &i, 10));
        CX_TEST_ASSERT(i == -42);

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

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

        CX_TEST_ASSERT(0 != cx_strtod(cx_str("13.37 "), &d));
        CX_TEST_ASSERT(0 != cx_strtod(cx_str(" 13.37"), &d));
        CX_TEST_ASSERT(0 == cx_strtod(cx_str("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_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);
    cx_test_register(suite, test_strchr);
    cx_test_register(suite, test_strrchr);
    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