diff options
| author | Joseph Hunkeler <jhunkeler@users.noreply.github.com> | 2024-06-20 15:10:56 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-20 15:10:56 -0400 | 
| commit | 931ee28eb9c5b5e3c2b0d3008f5f65d810dc9b0c (patch) | |
| tree | 5dbcccffd509fa71a99c351ed4628ed0841e1e46 /tests | |
| parent | 11aa1d44d95da221073e512fbec3bbccc0f1a46b (diff) | |
| download | stasis-931ee28eb9c5b5e3c2b0d3008f5f65d810dc9b0c.tar.gz | |
Unit tests (#6)
* Initial commit of unit tests [WIP]
* Address shortcomings and bugs flushed out by unit tests
* Enable unit testing in CI workflow
* Enable verbose ctests
* Handle lack of __FILE_NAME__ define
* Only podman support `run --arch` argument
* Skip docker build testing if CI system cannot pull an image
* Remove errant call to puts()
* Identify local repo user
* Fix missing xmllint
* NULL terminate arrays
* Fix filename assignment in is_url mode
* Break loop when expected lines are exhausted
* strcmp_array expects NULL terminated array. Iterating by size in this case passes NULL to strcmp leading to an invalid read
* Remove debug printf statements
* Disable a few warnings for tests
* Workaround for ctest junit xml truncation
* Update checkout@v4
* Prevent false-positive result
* Return zero on error
* Fix strlist_remove function
* Value argument can be constant
* Fix test to match changes to startswith and endswith
* Add test_ini.c
* Fix redaction code to accept NULL pointers in array
* And let the caller specify the length of the array of strings to redact.
* Redactions now occur directly on authentication strings rather than their command line arguments
* Fix BUILD_TESTING_DEBUG
* Adds missing -D argument
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/CMakeLists.txt | 32 | ||||
| -rw-r--r-- | tests/test_docker.c | 73 | ||||
| -rw-r--r-- | tests/test_ini.c | 108 | ||||
| -rw-r--r-- | tests/test_relocation.c | 62 | ||||
| -rw-r--r-- | tests/test_str.c | 377 | ||||
| -rw-r--r-- | tests/test_strlist.c | 617 | ||||
| -rw-r--r-- | tests/test_system.c | 145 | ||||
| -rw-r--r-- | tests/test_template.c | 100 | ||||
| -rw-r--r-- | tests/test_utils.c | 479 | ||||
| -rw-r--r-- | tests/testing.h | 174 | 
10 files changed, 2167 insertions, 0 deletions
| diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..5b45d58 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,32 @@ +include_directories( +        ${CMAKE_SOURCE_DIR}/include +        ${CMAKE_BINARY_DIR}/include +) +set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/tests) +set(CTEST_BINARY_DIRECTORY ${PROJECT_BINARY_DIR}/tests) +set(nix_gnu_cflags -Wno-error -Wno-unused-parameter -Wno-discarded-qualifiers) +set(nix_clang_cflags -Wno-unused-parameter -Wno-incompatible-pointer-types-discards-qualifiers) +set(win_msvc_cflags /Wall) + + +file(GLOB files "test_*.c") + +foreach(file ${files}) +    string(REGEX REPLACE "(^.*/|\\.[^.]*$)" "" file_without_ext ${file}) +    add_executable(${file_without_ext} ${file}) +    if (CMAKE_C_COMPILER_ID STREQUAL "GNU") +        target_compile_options(${file_without_ext} PRIVATE ${nix_cflags} ${nix_gnu_cflags}) +    elseif (CMAKE_C_COMPILER_ID MATCHES "Clang") +        target_compile_options(${file_without_ext} PRIVATE ${nix_cflags} ${nix_clang_cflags}) +    elseif (CMAKE_C_COMPILER_ID STREQUAL "MSVC") +        target_compile_options(${file_without_ext} PRIVATE ${win_cflags} ${win_msvc_cflags}) +    endif() +    target_link_libraries(${file_without_ext} PRIVATE ohmycal) +    add_test(${file_without_ext} ${file_without_ext}) +    set_tests_properties(${file_without_ext} +            PROPERTIES +            TIMEOUT 120) +    set_tests_properties(${file_without_ext} +            PROPERTIES +            SKIP_RETURN_CODE 127) +endforeach()
\ No newline at end of file diff --git a/tests/test_docker.c b/tests/test_docker.c new file mode 100644 index 0000000..eac385e --- /dev/null +++ b/tests/test_docker.c @@ -0,0 +1,73 @@ +#include "testing.h" +struct DockerCapabilities cap_suite; + +void test_docker_capable() { +    struct DockerCapabilities cap; +    int result = docker_capable(&cap); +    OMC_ASSERT(result == true, "docker installation unusable"); +    OMC_ASSERT(cap.available, "docker is not available"); +    OMC_ASSERT(cap.build != 0, "docker cannot build images"); +    // no cap.podman assertion. doesn't matter if we have docker or podman +    OMC_ASSERT(cap.usable, "docker is unusable"); +} + +void test_docker_exec() { +    OMC_ASSERT(docker_exec("system info", 0) == 0, "unable to print docker information"); +    OMC_ASSERT(docker_exec("system info", OMC_DOCKER_QUIET) == 0, "unable to be quiet"); +} + +void test_docker_sanitize_tag() { +    const char *data = "  !\"#$%&'()*+,-;<=>?@[\\]^_`{|}~"; +    char *input = strdup(data); +    docker_sanitize_tag(input); +    int result = 0; +    for (size_t i = 0; i < strlen(data); i++) { +        if (input[i] != '-') { +            result = 1; +            break; +        } +    } +    OMC_ASSERT(result == 0, "proper tag characters were not replaced correctly"); +    guard_free(input); +} + +void test_docker_build_and_script_and_save() { +    OMC_SKIP_IF(docker_exec("pull alpine:latest", OMC_DOCKER_QUIET), "unable to pull an image"); + +    const char *dockerfile_contents = "FROM alpine:latest\nCMD [\"sh\", \"-l\"]\n"; +    mkdir("test_docker_build", 0755); +    if (!pushd("test_docker_build")) { +        omc_testing_write_ascii("Dockerfile", dockerfile_contents); +        OMC_ASSERT(docker_build(".", "-t test_docker_build", cap_suite.build) == 0, "docker build test failed"); +        OMC_ASSERT(docker_script("test_docker_build", "uname -a", 0) == 0, "simple docker container script execution failed"); +        OMC_ASSERT(docker_save("test_docker_build", ".", OMC_DOCKER_IMAGE_COMPRESSION) == 0, "saving a simple image failed"); +        OMC_ASSERT(docker_exec("load < test_docker_build.tar.*", 0) == 0, "loading a simple image failed"); +        docker_exec("image rm -f test_docker_build", 0); +        remove("Dockerfile"); +        popd(); +        remove("test_docker_build"); +    } else { +        OMC_ASSERT(false, "dir change failed"); +        return; +    } +} + +void test_docker_validate_compression_program() { +    OMC_ASSERT(docker_validate_compression_program(OMC_DOCKER_IMAGE_COMPRESSION) == 0, "baked-in compression program does not exist"); +} + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    if (!docker_capable(&cap_suite)) { +        return OMC_TEST_SUITE_SKIP; +    } +    OMC_TEST_FUNC *tests[] = { +            test_docker_capable, +            test_docker_exec, +            test_docker_build_and_script_and_save, +            test_docker_sanitize_tag, +            test_docker_validate_compression_program, +    }; +    OMC_TEST_RUN(tests); +    OMC_TEST_END_MAIN(); +}
\ No newline at end of file diff --git a/tests/test_ini.c b/tests/test_ini.c new file mode 100644 index 0000000..f9f8234 --- /dev/null +++ b/tests/test_ini.c @@ -0,0 +1,108 @@ +#include "testing.h" +#include "ini.h" + +void test_ini_open_empty() { +    struct INIFILE *ini; +    ini = ini_open("/dev/null"); +    OMC_ASSERT(ini->section_count == 1, "should have at least one section pre-allocated"); +    OMC_ASSERT(ini->section != NULL, "default section not present"); +    ini_free(&ini); +    OMC_ASSERT(ini == NULL, "ini_free did not set pointer argument to NULL"); +} + +void test_ini_open() { +    const char *filename = "ini_open.ini"; +    const char *data = "[default]\na=1\nb=2\nc=3\n[section name]test=true\n"; +    struct INIFILE *ini; +    omc_testing_write_ascii(filename, data); +    ini = ini_open(filename); +    OMC_ASSERT(ini->section_count == 2, "should have two sections"); +    OMC_ASSERT(ini->section != NULL, "sections are not allocated"); +    ini_free(&ini); +    OMC_ASSERT(ini == NULL, "ini_free did not set pointer argument to NULL"); +    remove(filename); +} + +void test_ini_section_search() { +    const char *filename = "ini_open.ini"; +    const char *data = "[default]\na=1\nb=2\nc=3\n[section name here]\ntest=true\n"; +    struct INIFILE *ini; +    struct INISection *section; + +    omc_testing_write_ascii(filename, data); + +    ini = ini_open(filename); +    section = ini_section_search(&ini, INI_SEARCH_BEGINS, "section"); +    OMC_ASSERT(section->data_count == 1, "should have one data variable"); +    OMC_ASSERT(strcmp(section->data[0]->key, "test") == 0, "should have one data variable named 'test'"); +    OMC_ASSERT(strcmp(section->data[0]->value, "true") == 0, "'test' should be equal to 'true'"); +    OMC_ASSERT(strcmp(section->key, "section name here") == 0, "defined section not found"); +    section = ini_section_search(&ini, INI_SEARCH_SUBSTR, "name"); +    OMC_ASSERT(strcmp(section->data[0]->key, "test") == 0, "should have one data variable named 'test'"); +    OMC_ASSERT(strcmp(section->data[0]->value, "true") == 0, "'test' should be equal to 'true'"); +    OMC_ASSERT(strcmp(section->key, "section name here") == 0, "defined section not found"); +    section = ini_section_search(&ini, INI_SEARCH_EXACT, "section name here"); +    OMC_ASSERT(strcmp(section->data[0]->key, "test") == 0, "should have one data variable named 'test'"); +    OMC_ASSERT(strcmp(section->data[0]->value, "true") == 0, "'test' should be equal to 'true'"); +    OMC_ASSERT(strcmp(section->key, "section name here") == 0, "defined section not found"); + +    section = ini_section_search(&ini, INI_SEARCH_BEGINS, "bogus"); +    OMC_ASSERT(section == NULL, "bogus section search returned non-NULL"); +    section = ini_section_search(&ini, INI_SEARCH_BEGINS, NULL); +    OMC_ASSERT(section == NULL, "NULL argument returned non-NULL"); +    ini_free(&ini); +    remove(filename); +} + +void test_ini_has_key() { +    const char *filename = "ini_open.ini"; +    const char *data = "[default]\na=1\nb=2\nc=3\n[section name here]\ntest=true\n"; +    struct INIFILE *ini; + +    omc_testing_write_ascii(filename, data); + +    ini = ini_open(filename); +    OMC_ASSERT(ini_has_key(ini, "default", "a") == true, "'default:a' key should exist"); +    OMC_ASSERT(ini_has_key(NULL, "default", NULL) == false, "NULL ini object should return false"); +    OMC_ASSERT(ini_has_key(ini, "default", NULL) == false, "NULL key should return false"); +    OMC_ASSERT(ini_has_key(ini, NULL, "a") == false, "NULL section should return false"); +    OMC_ASSERT(ini_has_key(ini, "default", "noname") == false, "'default:noname' key should not exist"); +    OMC_ASSERT(ini_has_key(ini, "doesnotexist", "name") == false, "'doesnotexist:name' key should not exist"); +    ini_free(&ini); +    remove(filename); +} + +void test_ini_setval_getval() { +    const char *filename = "ini_open.ini"; +    const char *data = "[default]\na=1\nb=2\nc=3\n[section name here]\ntest=true\n"; +    struct INIFILE *ini; + +    omc_testing_write_ascii(filename, data); + +    ini = ini_open(filename); +    union INIVal val; +    OMC_ASSERT(ini_setval(&ini, INI_SETVAL_REPLACE, "default", "a", "changed") == 0, "failed to set value"); +    OMC_ASSERT(ini_getval(ini, "default", "a", INIVAL_TYPE_STR, &val) == 0, "failed to get value"); +    OMC_ASSERT(strcmp(val.as_char_p, "a") != 0, "unexpected value loaded from modified variable"); +    OMC_ASSERT(strcmp(val.as_char_p, "changed") == 0, "unexpected value loaded from modified variable"); + +    OMC_ASSERT(ini_setval(&ini, INI_SETVAL_APPEND, "default", "a", " twice") == 0, "failed to set value"); +    OMC_ASSERT(ini_getval(ini, "default", "a", INIVAL_TYPE_STR, &val) == 0, "failed to get value"); +    OMC_ASSERT(strcmp(val.as_char_p, "changed") != 0, "unexpected value loaded from modified variable"); +    OMC_ASSERT(strcmp(val.as_char_p, "changed twice") == 0, "unexpected value loaded from modified variable"); +    ini_free(&ini); +    remove(filename); +} + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    OMC_TEST_FUNC *tests[] = { +        test_ini_open_empty, +        test_ini_open, +        test_ini_section_search, +        test_ini_has_key, +        test_ini_setval_getval, +    }; +    OMC_TEST_RUN(tests); +    OMC_TEST_END_MAIN(); +}
\ No newline at end of file diff --git a/tests/test_relocation.c b/tests/test_relocation.c new file mode 100644 index 0000000..0796074 --- /dev/null +++ b/tests/test_relocation.c @@ -0,0 +1,62 @@ +#include "testing.h" + +static const char *test_string = "The quick brown fox jumps over the lazy dog."; +static const char *targets[] = { +        "The", "^^^ quick brown fox jumps over the lazy dog.", +        "quick", "The ^^^ brown fox jumps over the lazy dog.", +        "brown fox jumps over the", "The quick ^^^ lazy dog.", +        "lazy", "The quick brown fox jumps over the ^^^ dog.", +        "dog", "The quick brown fox jumps over the lazy ^^^.", +}; + +void test_replace_text() { +    for (size_t i = 0; i < sizeof(targets) / sizeof(*targets); i += 2) { +        const char *target = targets[i]; +        const char *expected = targets[i + 1]; +        char input[BUFSIZ] = {0}; +        strcpy(input, test_string); + +        OMC_ASSERT(replace_text(input, target, "^^^", 0) == 0, "string replacement failed"); +        OMC_ASSERT(strcmp(input, expected) == 0, "unexpected replacement"); +    } + +} + +void test_file_replace_text() { +    for (size_t i = 0; i < sizeof(targets) / sizeof(*targets); i += 2) { +        const char *filename = "test_file_replace_text.txt"; +        const char *target = targets[i]; +        const char *expected = targets[i + 1]; +        FILE *fp; + +        fp = fopen(filename, "w"); +        if (fp) { +            fprintf(fp, "%s", test_string); +            fclose(fp); +            OMC_ASSERT(file_replace_text(filename, target, "^^^", 0) == 0, "string replacement failed"); +        } else { +            OMC_ASSERT(false, "failed to open file for writing"); +            return; +        } + +        char input[BUFSIZ] = {0}; +        fp = fopen(filename, "r"); +        if (fp) { +            fread(input, sizeof(*input), sizeof(input), fp); +            OMC_ASSERT(strcmp(input, expected) == 0, "unexpected replacement"); +        } else { +            OMC_ASSERT(false, "failed to open file for reading"); +            return; +        } +    } +} + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    OMC_TEST_FUNC *tests[] = { +        test_replace_text, +        test_file_replace_text, +    }; +    OMC_TEST_RUN(tests); +    OMC_TEST_END_MAIN(); +}
\ No newline at end of file diff --git a/tests/test_str.c b/tests/test_str.c new file mode 100644 index 0000000..fc2cf46 --- /dev/null +++ b/tests/test_str.c @@ -0,0 +1,377 @@ +#include "testing.h" + +void test_to_short_version() { +    struct testcase { +        const char *data; +        const char *expected; +    }; + +    struct testcase tc[] = { +            {.data = "1.2.3", .expected = "123"}, +            {.data = "py3.12", .expected = "py312"}, +            {.data = "generic-1.2.3", .expected = "generic-123"}, +            {.data = "nothing to do", .expected = "nothing to do"}, +    }; + +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char *result = to_short_version(tc[i].data); +        OMC_ASSERT_FATAL(result != NULL, "should not be NULL"); +        OMC_ASSERT(strcmp(result, tc[i].expected) == 0, "unexpected result"); +        guard_free(result); +    } + +} + +void test_tolower_s() { +    struct testcase { +        const char *data; +        const char *expected; +    }; + +    struct testcase tc[] = { +        {.data = "HELLO WORLD", .expected = "hello world"}, +        {.data = "Hello World", .expected = "hello world"}, +        {.data = "hello world", .expected = "hello world"}, +    }; + +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char input[100] = {0}; +        strcpy(input, tc[i].data); +        tolower_s(input); +        OMC_ASSERT(strcmp(input, tc[i].expected) == 0, "unexpected result"); +    } +} + +void test_isdigit_s() { +    struct testcase { +        const char *data; +        int expected; +    }; + +    struct testcase tc[] = { +        {.data = "", .expected = false}, +        {.data = "1", .expected = true}, +        {.data = "1234567890", .expected = true}, +        {.data = " 1 ", .expected = false}, +        {.data = "a number", .expected = false}, +        {.data = "10 numbers", .expected = false}, +        {.data = "10 10", .expected = false} +    }; + +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        OMC_ASSERT(isdigit_s(tc[i].data) == tc[i].expected, "unexpected result"); +    } +} + +void test_strdup_array_and_strcmp_array() { +    struct testcase { +        const char **data; +        const char **expected; +    }; +    const struct testcase tc[] = { +        {.data = (const char *[]) {"abc", "123", NULL}, +         .expected = (const char *[]) {"abc", "123", NULL}}, +        {.data = (const char *[]) {"I", "have", "a", "pencil", "box", NULL}, +         .expected = (const char *[]) {"I", "have", "a", "pencil", "box", NULL}}, +    }; + +    OMC_ASSERT(strdup_array(NULL) == NULL, "returned non-NULL on NULL argument"); +    for (size_t outer = 0; outer < sizeof(tc) / sizeof(*tc); outer++) { +        char **result = strdup_array((char **) tc[outer].data); +        OMC_ASSERT(strcmp_array((const char **) result, tc[outer].expected) == 0, "array members were different"); +    } + +    const struct testcase tc_bad[] = { +            {.data = (const char *[]) {"ab", "123", NULL}, +                    .expected = (const char *[]) {"abc", "123", NULL}}, +            {.data = (const char *[]) {"have", "a", "pencil", "box", NULL}, +                    .expected = (const char *[]) {"I", "have", "a", "pencil", "box", NULL}}, +    }; + +    OMC_ASSERT(strcmp_array(tc[0].data, NULL) > 0, "right argument is NULL, expected positive return"); +    OMC_ASSERT(strcmp_array(NULL, tc[0].data) < 0, "left argument is NULL, expected negative return"); +    OMC_ASSERT(strcmp_array(NULL, NULL) == 0, "left and right arguments are NULL, expected zero return"); +    for (size_t outer = 0; outer < sizeof(tc_bad) / sizeof(*tc_bad); outer++) { +        char **result = strdup_array((char **) tc_bad[outer].data); +        OMC_ASSERT(strcmp_array((const char **) result, tc_bad[outer].expected) != 0, "array members were identical"); +    } +} + +void test_strchrdel() { +    struct testcase { +        const char *data; +        const char *input; +        const char *expected; +    }; +    const struct testcase tc[] = { +            {.data ="aaaabbbbcccc", .input = "ac", .expected = "bbbb"}, +            {.data = "1I 2have 3a 4pencil 5box.", .input = "1245", .expected = "I have 3a pencil box."}, +            {.data = "\v\v\vI\t\f  ha\tve   a\t    pen\tcil     b\tox.", .input = " \f\t\v", "Ihaveapencilbox."}, +    }; + +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char *data = strdup(tc[i].data); +        strchrdel(data, tc[i].input); +        OMC_ASSERT(strcmp(data, tc[i].expected) == 0, "wrong status for condition"); +        guard_free(data); +    } +} + +void test_endswith() { +    struct testcase { +        const char *data; +        const char *input; +        const int expected; +    }; +    struct testcase tc[] = { +            {.data = "I have a pencil box.", .input = ".", .expected = true}, +            {.data = "I have a pencil box.", .input = "box.", .expected = true}, +            {.data = "I have a pencil box.", .input = "pencil box.", .expected = true}, +            {.data = "I have a pencil box.", .input = "a pencil box.", .expected = true}, +            {.data = "I have a pencil box.", .input = "have a pencil box.", .expected = true}, +            {.data = "I have a pencil box.", .input = "I have a pencil box.", .expected = true}, +            {.data = ".    ", .input = ".", .expected = false}, +            {.data = "I have a pencil box.", .input = "pencil box", .expected = false}, +            {.data = NULL, .input = "test", .expected = false}, +            {.data = "I have a pencil box", .input = NULL, .expected = false}, +            {.data = NULL, .input = NULL, .expected = false}, +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        int result = endswith(tc[i].data, tc[i].input); +        OMC_ASSERT(result == tc[i].expected, "wrong status for condition"); +    } +} + +void test_startswith() { +    struct testcase { +        const char *data; +        const char *input; +        const int expected; +    }; +    struct testcase tc[] = { +            {.data = "I have a pencil box.", .input = "I", .expected = true}, +            {.data = "I have a pencil box.", .input = "I have", .expected = true}, +            {.data = "I have a pencil box.", .input = "I have a", .expected = true}, +            {.data = "I have a pencil box.", .input = "I have a pencil", .expected = true}, +            {.data = "I have a pencil box.", .input = "I have a pencil box", .expected = true}, +            {.data = "I have a pencil box.", .input = "I have a pencil box.", .expected = true}, +            {.data = "    I have a pencil box.", .input = "I have", .expected = false}, +            {.data = "I have a pencil box.", .input = "pencil box", .expected = false}, +            {.data = NULL, .input = "test", .expected = false}, +            {.data = "I have a pencil box", .input = NULL, .expected = false}, +            {.data = NULL, .input = NULL, .expected = false}, +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        int result = startswith(tc[i].data, tc[i].input); +        OMC_ASSERT(result == tc[i].expected, "wrong status for condition"); +    } +} + +void test_num_chars() { +    struct testcase { +        const char *data; +        const char input; +        const int expected; +    }; +    struct testcase tc[] = { +            {.data = "         ", .input = ' ', .expected = 9}, +            {.data = "1 1 1 1 1", .input = '1', .expected = 5}, +            {.data = "abc\t\ndef\nabc\ndef\n", .input = 'c', .expected = 2}, +            {.data = "abc\t\ndef\nabc\ndef\n", .input = '\t', .expected = 1}, +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        OMC_ASSERT(num_chars(tc[i].data, tc[i].input) == tc[i].expected, "incorrect number of characters detected"); +    } +} + +void test_split() { +    struct testcase { +            const char *data; +            const size_t max_split; +            const char *delim; +            const char **expected; +    }; +    struct testcase tc[] = { +            {.data = "a/b/c/d/e", .delim = "/", .max_split = 0, .expected = (const char *[]) {"a", "b", "c", "d", "e", NULL}}, +            {.data = "a/b/c/d/e", .delim = "/", .max_split = 1, .expected = (const char *[]) {"a", "b/c/d/e", NULL}}, +            {.data = "a/b/c/d/e", .delim = "/", .max_split = 2, .expected = (const char *[]) {"a", "b", "c/d/e", NULL}}, +            {.data = "a/b/c/d/e", .delim = "/", .max_split = 3, .expected = (const char *[]) {"a", "b", "c", "d/e", NULL}}, +            {.data = "a/b/c/d/e", .delim = "/", .max_split = 4, .expected = (const char *[]) {"a", "b", "c", "d", "e", NULL}}, +            {.data = "multiple words split n times", .delim = " ", .max_split = 0, .expected = (const char *[]) {"multiple", "words", "split", "n", "times", NULL}}, +            {.data = "multiple words split n times", .delim = " ", .max_split = 1, .expected = (const char *[]) {"multiple", "words split n times", NULL}}, +            {.data = NULL, .delim = NULL, NULL}, +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char **result; +        result = split(tc[i].data, tc[i].delim, tc[i].max_split); +        OMC_ASSERT(strcmp_array(result, tc[i].expected) == 0, "Split failed"); +        GENERIC_ARRAY_FREE(result); +    } +} + +void test_join() { +    struct testcase { +        const char **data; +        const char *delim; +        const char *expected; +    }; +    struct testcase tc[] = { +            {.data = (const char *[]) {"a", "b", "c", "d", "e", NULL}, .delim = "", .expected = "abcde"}, +            {.data = (const char *[]) {"a", NULL}, .delim = "", .expected = "a"}, +            {.data = (const char *[]) {"a", "word", "or", "two", NULL}, .delim = "/", .expected = "a/word/or/two"}, +            {.data = NULL, .delim = NULL, .expected = ""}, +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char *result; +        result = join(tc[i].data, tc[i].delim); +        OMC_ASSERT(strcmp(result ? result : "", tc[i].expected) == 0, "failed to join array"); +        guard_free(result); +    } +} + +void test_join_ex() { +    struct testcase { +        const char *delim; +        const char *expected; +    }; +    struct testcase tc[] = { +            {.delim = "", .expected = "abcde"}, +            {.delim = "/", .expected = "a/b/c/d/e"}, +            {.delim = "\n", .expected = "a\nb\nc\nd\ne"}, +            {.delim = "\n\n", .expected = "a\n\nb\n\nc\n\nd\n\ne"}, +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char *result; +        result = join_ex(tc[i].delim, "a", "b", "c", "d", "e", NULL); +        OMC_ASSERT(strcmp(result ? result : "", tc[i].expected) == 0, "failed to join array"); +        guard_free(result); +    } +} + +void test_substring_between() { +    struct testcase { +        const char *data; +        const char *delim; +        const char *expected; +    }; +    struct testcase tc[] = { +            {.data = "I like {{adjective}} spaceships", .delim = "{{}}", .expected = "adjective"}, +            {.data = "I like {adjective} spaceships", .delim = "{}", .expected = "adjective"}, +            {.data = "one = 'two'", .delim = "''", .expected = "two"}, +            {.data = "I like {adjective> spaceships", .delim = "{}", .expected = ""}, // no match +            {.data = NULL, .delim = NULL, .expected = ""}, // both arguments NULL +            {.data = "nothing!", .delim = NULL, .expected = ""}, // null delim +            {.data = "", .delim = "{}", .expected = ""}, // no match +            {.data = NULL, .delim = "nothing!", .expected = ""}, // null sptr +            {.data = "()", .delim = "(", .expected = ""}, // delim not divisible by 2 +            {.data = "()", .delim = "( )", .expected = ""}, // delim not divisible by 2 +            {.data = "nothing () here", .delim = "()", .expected = ""}, // nothing exists between delimiters +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char *result = substring_between(tc[i].data, tc[i].delim); +        OMC_ASSERT(strcmp(result ? result : "", tc[i].expected) == 0, "unable to extract substring"); +        guard_free(result); +    } +} + +void test_strdeldup() { +    struct testcase { +        const char **data; +        const char **expected; +    }; +    struct testcase tc[] = { +        {.data = NULL, .expected = NULL}, +        {.data = (const char *[]) {"a", "a", "a", "b", "b", "b", "c", "c", "c", NULL}, .expected = (const char *[]) {"a", "b", "c", NULL}}, +        {.data = (const char *[]) {"a", "b", "c", "a", "b", "c", "a", "b", "c", NULL}, .expected = (const char *[]) {"a", "b", "c", NULL}}, +        {.data = (const char *[]) {"apple", "banana", "coconut", NULL}, .expected = (const char *[]) {"apple", "banana", "coconut", NULL}}, +        {.data = (const char *[]) {"apple", "banana", "apple", "coconut", NULL}, .expected = (const char *[]) {"apple", "banana", "coconut", NULL}}, +    }; +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char **result = strdeldup(tc[i].data); +        OMC_ASSERT(strcmp_array(result, tc[i].expected) == 0, "incorrect number of duplicates removed"); +        GENERIC_ARRAY_FREE(result); +    } +} + +void test_strsort() { + +} + +void test_strstr_array() { + +} + +void test_lstrip() { +    struct testcase { +        const char *data; +        const char *expected; +    }; +    struct testcase tc[] = { +        {.data = "I am a string.\n", .expected = "I am a string.\n"}, // left strip only +        {.data = "I am a string.\n", .expected = "I am a string.\n"}, +        {.data = "\t\t\t\tI am a string.\n", .expected = "I am a string.\n"}, +        {.data = "    I    am a string.\n", .expected = "I    am a string.\n"}, +    }; + +    OMC_ASSERT(lstrip(NULL) == NULL, "incorrect return type"); +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char *buf = calloc(255, sizeof(*buf)); +        char *result; +        strcpy(buf, tc[i].data); +        result = lstrip(buf); +        OMC_ASSERT(strcmp(result ? result : "", tc[i].expected) == 0, "incorrect strip-from-left"); +        guard_free(buf); +    } +} + +void test_strip() { +    struct testcase { +        const char *data; +        const char *expected; +    }; +    struct testcase tc[] = { +            {.data = "  I am a string.\n", .expected = "  I am a string."}, // right strip only +            {.data = "\tI am a string.\n\t", .expected = "\tI am a string."}, +            {.data = "\t\t\t\tI am a string.\n", .expected = "\t\t\t\tI am a string."}, +            {.data = "I am a string.\n\n\n\n", .expected = "I am a string."}, +            {.data = "", .expected = ""}, +            {.data = " ", .expected = ""}, +    }; + +    OMC_ASSERT(strip(NULL) == NULL, "incorrect return type"); +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        char *buf = calloc(255, sizeof(*buf)); +        char *result; +        strcpy(buf, tc[i].data); +        result = strip(buf); +        OMC_ASSERT(strcmp(result ? result : "", tc[i].expected) == 0, "incorrect strip-from-right"); +        guard_free(buf); +    } +} + +void test_isempty() { + +} + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    OMC_TEST_FUNC *tests[] = { +            test_to_short_version, +            test_tolower_s, +            test_isdigit_s, +            test_strdup_array_and_strcmp_array, +            test_strchrdel, +            test_strdeldup, +            test_lstrip, +            test_strip, +            test_num_chars, +            test_startswith, +            test_endswith, +            test_split, +            test_join, +            test_join_ex, +            test_substring_between, +    }; +    OMC_TEST_RUN(tests); +    OMC_TEST_END_MAIN(); +}
\ No newline at end of file diff --git a/tests/test_strlist.c b/tests/test_strlist.c new file mode 100644 index 0000000..aa9eb4e --- /dev/null +++ b/tests/test_strlist.c @@ -0,0 +1,617 @@ +#include "testing.h" + +#define BOILERPLATE_TEST_STRLIST_AS_TYPE(FUNC, TYPE) \ +do { \ +    struct StrList *list; \ +    list = strlist_init(); \ +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { \ +        strlist_append(&list, (char *) tc[i].data); \ +        result = FUNC(list, i);                      \ +        if (!strlist_errno) { \ +            OMC_ASSERT(result == (TYPE) tc[i].expected, "unexpected numeric result"); \ +        } \ +    } \ +    guard_strlist_free(&list); \ +} while (0) + +void test_strlist_init() { +    struct StrList *list; +    list = strlist_init(); +    OMC_ASSERT(list != NULL, "list should not be NULL"); +    OMC_ASSERT(list->num_alloc == 1, "fresh list should have one record allocated"); +    OMC_ASSERT(list->num_inuse == 0, "fresh list should have no records in use"); +    guard_strlist_free(&list); +} + +void test_strlist_free() { +    struct StrList *list; +    list = strlist_init(); +    strlist_free(&list); +    // Not entirely sure what to assert here. On failure, it'll probably segfault. +} + +void test_strlist_append() { +    struct testcase { +        const char *data; +        const size_t expected_in_use; +        const char *expected; +    }; + +    struct testcase tc[] = { +            {.data = "I have a pencil box.", .expected_in_use = 1}, +            {.data = "A rectangular pencil box.", .expected_in_use = 2}, +            {.data = "If I need a pencil I know where to get one.", .expected_in_use = 3}, +    }; + +    struct StrList *list; +    list = strlist_init(); +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        strlist_append(&list, (char *) tc[i].data); +        OMC_ASSERT(list->num_inuse == tc[i].expected_in_use, "incorrect number of records in use"); +        OMC_ASSERT(list->num_alloc == tc[i].expected_in_use + 1, "incorrect number of records allocated"); +        OMC_ASSERT(strcmp(strlist_item(list, i), tc[i].data) == 0, "value was appended incorrectly. data mismatch."); +    } +    guard_strlist_free(&list); +} + +void test_strlist_append_many_records() { +    struct StrList *list; +    list = strlist_init(); +    size_t maxrec = 10240; +    const char *data = "There are many strings but this one is mine."; +    for (size_t i = 0; i < maxrec; i++) { +        strlist_append(&list, (char *) data); +    } +    OMC_ASSERT(strlist_count(list) == maxrec, "record count mismatch"); +    guard_strlist_free(&list); +} + +void test_strlist_set() { +    struct StrList *list; +    list = strlist_init(); +    size_t maxrec = 100; +    const char *data = "There are many strings but this one is mine."; +    const char *replacement_short = "A shorter string."; +    const char *replacement_long = "A longer string ensures the array grows correctly."; +    int set_resize_long_all_ok = 0; +    int set_resize_short_all_ok = 0; + +    for (size_t i = 0; i < maxrec; i++) { +        strlist_append(&list, (char *) data); +    } + +    for (size_t i = 0; i < maxrec; i++) { +        int result; +        strlist_set(&list, i, (char *) replacement_long); +        result = strcmp(strlist_item(list, i), replacement_long) == 0; +        set_resize_long_all_ok += result; +    } +    OMC_ASSERT(set_resize_long_all_ok == (int) maxrec, "Replacing record with longer strings failed"); + +    for (size_t i = 0; i < maxrec; i++) { +        int result; +        strlist_set(&list, i, (char *) replacement_short); +        result = strcmp(strlist_item(list, i), replacement_short) == 0; +        set_resize_short_all_ok += result; +    } +    OMC_ASSERT(set_resize_short_all_ok == (int) maxrec, "Replacing record with shorter strings failed"); + +    OMC_ASSERT(strlist_count(list) == maxrec, "record count mismatch"); +    guard_strlist_free(&list); +} + +void test_strlist_append_file() { +    struct testcase { +        const char *origin; +        const char **expected; +    }; +    const char *expected[] = { +            "Do not delete this file.\n", +            "Do not delete this file.\n", +            "Do not delete this file.\n", +            "Do not delete this file.\n", +            NULL, +    }; +    const char *local_filename = "test_strlist_append_file.txt"; + +    struct testcase tc[] = { +            {.origin = "https://ssb.stsci.edu/jhunk/omc_test/test_strlist_append_file_from_remote.txt", .expected = expected}, +            {.origin = local_filename, .expected = expected}, +    }; + +    FILE *fp; +    fp = fopen(local_filename, "w"); +    if (!fp) { +        OMC_ASSERT(false, "unable to open local file for writing"); +    } +    for (size_t i = 0; expected[i] != NULL; i++) { +        fprintf(fp, "%s", expected[i]); +    } +    fclose(fp); + +    for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { +        struct StrList *list; +        list = strlist_init(); +        if (!list) { +            OMC_ASSERT(false, "failed to create list"); +            return; +        } +        strlist_append_file(list, (char *) tc[i].origin, NULL); +        for (size_t z = 0; z < strlist_count(list); z++) { +            const char *left; +            const char *right; +            left = strlist_item(list, z); +            right = expected[z]; +            OMC_ASSERT(strcmp(left, right) == 0, "file content is different than expected"); +        } +        OMC_ASSERT(strcmp_array(list->data, expected) == 0, "file contents does not match expected values"); +        guard_strlist_free(&list); +    } +} + +void test_strlist_append_strlist() { +    struct StrList *left; +    struct StrList *right; +    left = strlist_init(); +    right = strlist_init(); + +    strlist_append(&right, "A"); +    strlist_append(&right, "B"); +    strlist_append(&right, "C"); +    strlist_append_strlist(left, right); +    OMC_ASSERT(strlist_cmp(left, right) == 0, "left and right should be identical"); +    strlist_append_strlist(left, right); +    OMC_ASSERT(strlist_cmp(left, right) == 1, "left and right should be different"); +    OMC_ASSERT(strlist_count(left) > strlist_count(right), "left should be larger than right"); +    guard_strlist_free(&left); +    guard_strlist_free(&right); +} + +void test_strlist_append_array() { +    const char *data[] = { +            "Appending", +            "Some", +            "Data", +            NULL, +    }; +    struct StrList *list; +    list = strlist_init(); +    strlist_append_array(list, (char **) data); +    size_t result = 0; +    for (size_t i = 0; i < strlist_count(list); i++) { +        char *item = strlist_item(list, i); +        result += strcmp(item, data[i]) == 0; +    } +    OMC_ASSERT(result == strlist_count(list), "result is not identical to input"); +    guard_strlist_free(&list); +} + +void test_strlist_append_tokenize() { +    const char *data = "This is a test.\nWe will split this string\ninto three list records\n"; +    size_t line_count = num_chars(data, '\n'); +    struct StrList *list; +    list = strlist_init(); +    strlist_append_tokenize(list, (char *) data, "\n"); +    // note: the trailing '\n' in the data is parsed as a token +    OMC_ASSERT(line_count + 1 == strlist_count(list), "unexpected number of lines in array"); +    int trailing_item_is_empty = isempty(strlist_item(list, strlist_count(list) - 1)); +    OMC_ASSERT(trailing_item_is_empty, "trailing record should be an empty string"); +    guard_strlist_free(&list); +} + +void test_strlist_copy() { +    struct StrList *list = strlist_init(); +    struct StrList *list_copy; + +    strlist_append(&list, "Make a copy"); +    strlist_append(&list, "of this data"); +    strlist_append(&list, "1:1, please"); + +    OMC_ASSERT(strlist_copy(NULL) == NULL, "NULL argument should return NULL"); + +    list_copy = strlist_copy(list); +    OMC_ASSERT(strlist_cmp(list, list_copy) == 0, "list was copied incorrectly"); +    guard_strlist_free(&list); +    guard_strlist_free(&list_copy); +} + +void test_strlist_remove() { +    const char *data[] = { +        "keep this", +        "remove this", +        "keep this", +        "remove this", +        "keep this", +        "remove this", +    }; +    struct StrList *list; +    list = strlist_init(); +    size_t data_size = sizeof(data) / sizeof(*data); +    for (size_t i = 0; i < data_size; i++) { +        strlist_append(&list, (char *) data[i]); +    } + +    size_t len_before = strlist_count(list); +    size_t removed = 0; +    for (size_t i = 0; i < len_before; i++) { +        char *item = strlist_item(list, i); +        if (startswith(item, "remove")) { +            strlist_remove(list, i); +            removed++; +        } +    } +    size_t len_after = strlist_count(list); +    OMC_ASSERT(len_before == data_size, "list length before modification is incorrect. new records?"); +    OMC_ASSERT(len_after == (len_before - removed), "list length after modification is incorrect. not enough records removed."); +     +    guard_strlist_free(&list); +} + +void test_strlist_cmp() { +    struct StrList *left; +    struct StrList *right; + +    left = strlist_init(); +    right = strlist_init(); +    OMC_ASSERT(strlist_cmp(NULL, NULL) == -1, "NULL first argument should return -1"); +    OMC_ASSERT(strlist_cmp(left, NULL) == -2, "NULL second argument should return -2"); +    OMC_ASSERT(strlist_cmp(left, right) == 0, "should be the same"); +    strlist_append(&left, "left"); +    OMC_ASSERT(strlist_cmp(left, right) == 1, "should return 1"); +    strlist_append(&right, "right"); +    OMC_ASSERT(strlist_cmp(left, right) == 1, "should return 1"); +    strlist_append(&left, "left again"); +    strlist_append(&left, "left again"); +    strlist_append(&left, "left again"); +    OMC_ASSERT(strlist_cmp(left, right) == 1, "should return 1"); +    OMC_ASSERT(strlist_cmp(right, left) == 1, "should return 1"); +    guard_strlist_free(&left); +    guard_strlist_free(&right); +} + +void test_strlist_reverse() { +    const char *data[] = { +        "a", "b", "c", "d", NULL +    }; +    const char *expected[] = { +        "d", "c", "b", "a", NULL +    }; +    struct StrList *list; +    list = strlist_init(); +    strlist_append_array(list, (char **) data); + +    strlist_reverse(list); +    int result = 0; +    for (size_t i = 0; i < strlist_count(list); i++) { +        char *item = strlist_item(list, i); +        result += strcmp(item, expected[i]) == 0; +    } +    OMC_ASSERT(result == (int) strlist_count(list), "alphabetic sort failed"); +    guard_strlist_free(&list); +} + +void test_strlist_count() { +    struct StrList *list; +    list = strlist_init(); +    strlist_append(&list, "abc"); +    strlist_append(&list, "123"); +    strlist_append(&list, "def"); +    strlist_append(&list, "456"); +    OMC_ASSERT(strlist_count(NULL) == 0, "NULL input should produce a zero result"); +    OMC_ASSERT(strlist_count(list) == 4, "incorrect record count"); + +    guard_strlist_free(&list); +} + +void test_strlist_sort_alphabetic() { +    const char *data[] = { +        "b", "c", "d", "a", NULL +    }; +    const char *expected[] = { +        "a", "b", "c", "d", NULL +    }; +    struct StrList *list; +    list = strlist_init(); +    strlist_append_array(list, (char **) data); + +    strlist_sort(list, OMC_SORT_ALPHA); +    int result = 0; +    for (size_t i = 0; i < strlist_count(list); i++) { +        char *item = strlist_item(list, i); +        result += strcmp(item, expected[i]) == 0; +    } +    OMC_ASSERT(result == (int) strlist_count(list), "alphabetic sort failed"); +    guard_strlist_free(&list); +} + +void test_strlist_sort_len_ascending() { +    const char *data[] = { +        "bb", "ccc", "dddd", "a", NULL +    }; +    const char *expected[] = { +        "a", "bb", "ccc", "dddd", NULL +    }; +    struct StrList *list; +    list = strlist_init(); +    strlist_append_array(list, (char **) data); + +    strlist_sort(list, OMC_SORT_LEN_ASCENDING); +    int result = 0; +    for (size_t i = 0; i < strlist_count(list); i++) { +        char *item = strlist_item(list, i); +        result += strcmp(item, expected[i]) == 0; +    } +    OMC_ASSERT(result == (int) strlist_count(list), "ascending sort failed"); +    guard_strlist_free(&list); +} + +void test_strlist_sort_len_descending() { +    const char *data[] = { +        "bb", "ccc", "dddd", "a", NULL +    }; +    const char *expected[] = { +        "dddd", "ccc", "bb", "a", NULL +    }; +    struct StrList *list; +    list = strlist_init(); +    strlist_append_array(list, (char **) data); + +    strlist_sort(list, OMC_SORT_LEN_DESCENDING); +    int result = 0; +    for (size_t i = 0; i < strlist_count(list); i++) { +        char *item = strlist_item(list, i); +        result += strcmp(item, expected[i]) == 0; +    } +    OMC_ASSERT(result == (int) strlist_count(list), "descending sort failed"); +    guard_strlist_free(&list); +} + +void test_strlist_item_as_str() { +    struct StrList *list; +    list = strlist_init(); +    strlist_append(&list, "hello"); +    OMC_ASSERT(strcmp(strlist_item_as_str(list, 0), "hello") == 0, "unexpected string result"); +    guard_strlist_free(&list); +} + +void test_strlist_item_as_char() { +    char result = 0; +    struct testcase { +        const char *data; +        const char expected; +    }; +    struct testcase tc[] = { +            {.data = "-129", .expected = 127}, // rollover +            {.data = "-128", .expected = -128}, +            {.data = "-1", .expected = -1}, +            {.data = "1", .expected = 1}, +            {.data = "127", .expected = 127}, +            {.data = "128", .expected = -128}, // rollover +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_char, char); +} + +void test_strlist_item_as_uchar() { +    int result; +    struct testcase { +        const char *data; +        const unsigned char expected; +    }; +    struct testcase tc[] = { +        {.data = "-1", .expected = 255}, +        {.data = "1", .expected = 1}, +        {.data = "255", .expected = 255}, +        {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_uchar, unsigned char); +} + +void test_strlist_item_as_short() { +    int result; +    struct testcase { +        const char *data; +        const short expected; +    }; +    struct testcase tc[] = { +            {.data = "-32769", .expected = 32767}, // rollover +            {.data = "-32768", .expected = -32768}, +            {.data = "-1", .expected = -1}, +            {.data = "1", .expected = 1}, +            {.data = "32767", .expected = 32767}, +            {.data = "32768", .expected = -32768}, // rollover +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_short, short); +} + +void test_strlist_item_as_ushort() { +    int result; +    struct testcase { +        const char *data; +        const unsigned short expected; +    }; +    struct testcase tc[] = { +            {.data = "-1", .expected = 65535}, +            {.data = "1", .expected = 1}, +            {.data = "65535", .expected = 65535}, +            {.data = "65536", .expected = 0}, // rollover +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_ushort, unsigned short); +} + +// From here on the values are different between architectures. Do very basic tests. +void test_strlist_item_as_int() { +    int result; +    struct testcase { +        const char *data; +        const int expected; +    }; +    struct testcase tc[] = { +            {.data = "-1", .expected = -1}, +            {.data = "1", .expected = 1}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_int, int); +} + +void test_strlist_item_as_uint() { +    unsigned int result; +    struct testcase { +        const char *data; +        const unsigned int expected; +    }; +    struct testcase tc[] = { +            {.data = "-1", .expected = UINT_MAX}, +            {.data = "1", .expected = 1}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_uint, unsigned int); +} + +void test_strlist_item_as_long() { +    long result; +    struct testcase { +        const char *data; +        const long expected; +    }; +    struct testcase tc[] = { +            {.data = "-1", .expected = -1}, +            {.data = "1", .expected = 1}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_long, long); +} + +void test_strlist_item_as_ulong() { +    unsigned long result; +    struct testcase { +        const char *data; +        const unsigned long expected; +    }; +    struct testcase tc[] = { +            {.data = "-1", .expected = ULONG_MAX}, +            {.data = "1", .expected = 1}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_ulong, unsigned long); +} + +void test_strlist_item_as_long_long() { +    long long result; +    struct testcase { +        const char *data; +        const long long expected; +    }; +    struct testcase tc[] = { +            {.data = "-1", .expected = -1}, +            {.data = "1", .expected = 1}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_long_long, long long); +} + +void test_strlist_item_as_ulong_long() { +    unsigned long long result; +    struct testcase { +        const char *data; +        const unsigned long long expected; +    }; +    struct testcase tc[] = { +            {.data = "-1", .expected = ULLONG_MAX}, +            {.data = "1", .expected = 1}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_ulong_long, unsigned long long); +} + +void test_strlist_item_as_float() { +    float result; +    struct testcase { +        const char *data; +        const float expected; +    }; +    struct testcase tc[] = { +            {.data = "1.0", .expected = 1.0f}, +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_float, float); +} + +void test_strlist_item_as_double() { +    double result; +    struct testcase { +        const char *data; +        const double expected; +    }; +    struct testcase tc[] = { +            {.data = "1.0", .expected = 1.0f}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_float, double); +} + +void test_strlist_item_as_long_double() { +    long double result; +    struct testcase { +        const char *data; +        const long double expected; +    }; +    struct testcase tc[] = { +            {.data = "1.0", .expected = 1.0f}, +            {.data = "abc", .expected = 0}, // error +    }; + +    BOILERPLATE_TEST_STRLIST_AS_TYPE(strlist_item_as_float, long double); +} + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    OMC_TEST_FUNC *tests[] = { +        test_strlist_init, +        test_strlist_free, +        test_strlist_append, +        test_strlist_append_many_records, +        test_strlist_set, +        test_strlist_append_file, +        test_strlist_append_strlist, +        test_strlist_append_tokenize, +        test_strlist_append_array, +        test_strlist_copy, +        test_strlist_remove, +        test_strlist_cmp, +        test_strlist_sort_alphabetic, +        test_strlist_sort_len_ascending, +        test_strlist_sort_len_descending, +        test_strlist_reverse, +        test_strlist_count, +        test_strlist_item_as_str, +        test_strlist_item_as_char, +        test_strlist_item_as_uchar, +        test_strlist_item_as_short, +        test_strlist_item_as_ushort, +        test_strlist_item_as_int, +        test_strlist_item_as_uint, +        test_strlist_item_as_long, +        test_strlist_item_as_ulong, +        test_strlist_item_as_long_long, +        test_strlist_item_as_ulong_long, +        test_strlist_item_as_float, +        test_strlist_item_as_double, +        test_strlist_item_as_long_double, +    }; +    OMC_TEST_RUN(tests); +    OMC_TEST_END_MAIN(); +}
\ No newline at end of file diff --git a/tests/test_system.c b/tests/test_system.c new file mode 100644 index 0000000..bf60222 --- /dev/null +++ b/tests/test_system.c @@ -0,0 +1,145 @@ +#include "testing.h" + +static int ascii_file_contains(const char *filename, const char *value) { +    int result = -1; +    char *contents = omc_testing_read_ascii(filename); +    if (!contents) { +        perror(filename); +        return result; +    } +    result = strcmp(contents, value) == 0; +    guard_free(contents); +    return result; +} + +void test_shell_output_null_args() { +    char *result; +    int status; +    result = shell_output(NULL, &status); +    OMC_ASSERT(strcmp(result, "") == 0, "no output expected"); +    OMC_ASSERT(status != 0, "expected a non-zero exit code due to null argument string"); +} + +void test_shell_output_non_zero_exit() { +    char *result; +    int status; +    result = shell_output("HELLO1234 WORLD", &status); +    OMC_ASSERT(strcmp(result, "") == 0, "no output expected"); +    OMC_ASSERT(status != 0, "expected a non-zero exit code due to intentional error"); +} + +void test_shell_output() { +    char *result; +    int status; +    result = shell_output("echo HELLO WORLD", &status); +    OMC_ASSERT(strcmp(result, "HELLO WORLD\n") == 0, "output message was incorrect"); +    OMC_ASSERT(status == 0, "expected zero exit code"); +} + +void test_shell_safe() { +    struct Process proc; +    memset(&proc, 0, sizeof(proc)); +    shell_safe(&proc, "true"); +    OMC_ASSERT(proc.returncode == 0, "expected a zero exit code"); +} + +void test_shell_safe_verify_restrictions() { +    struct Process proc; + +    const char *invalid_chars = OMC_SHELL_SAFE_RESTRICT; +    for (size_t i = 0; i < strlen(invalid_chars); i++) { +        char cmd[PATH_MAX] = {0}; +        memset(&proc, 0, sizeof(proc)); + +        sprintf(cmd, "true%c false", invalid_chars[i]); +        shell_safe(&proc, cmd); +        OMC_ASSERT(proc.returncode == -1, "expected a negative result due to intentional error"); +    } +} + +void test_shell_null_proc() { +    int returncode = shell(NULL, "true"); +    OMC_ASSERT(returncode == 0, "expected a zero exit code"); +} + +void test_shell_null_args() { +    struct Process proc; +    memset(&proc, 0, sizeof(proc)); +    shell(&proc, NULL); +    OMC_ASSERT(proc.returncode < 0, "expected a non-zero/negative exit code"); +} + +void test_shell_exit() { +    struct Process proc; +    memset(&proc, 0, sizeof(proc)); +    shell(&proc, "true"); +    OMC_ASSERT(proc.returncode == 0, "expected a zero exit code"); +} + +void test_shell_non_zero_exit() { +    struct Process proc; +    memset(&proc, 0, sizeof(proc)); +    shell(&proc, "false"); +    OMC_ASSERT(proc.returncode != 0, "expected a non-zero exit code"); +} + +void test_shell() { +    struct Process procs[] = { +            {.f_stdout = "", .f_stderr = "", .redirect_stderr = 0, .returncode = 0}, +            {.f_stdout = "stdout.log", .f_stderr = "", .redirect_stderr = 0, .returncode = 0}, +            {.f_stdout = "", .f_stderr = "stderr.log", .redirect_stderr = 0, .returncode = 0}, +            {.f_stdout = "stdouterr.log", .f_stderr = "", .redirect_stderr = 1, .returncode = 0}, +    }; +    for (size_t i = 0; i < sizeof(procs) / sizeof(*procs); i++) { +        shell(&procs[i], "echo test_stdout; echo test_stderr >&2"); +        struct Process *proc = &procs[i]; +        switch (i) { +            case 0: +                OMC_ASSERT(proc->returncode == 0, "echo should not fail"); +                break; +            case 1: +                OMC_ASSERT(proc->returncode == 0, "echo should not fail"); +                OMC_ASSERT(access(proc->f_stdout, F_OK) == 0, "stdout.log should exist"); +                OMC_ASSERT(access(proc->f_stderr, F_OK) != 0, "stderr.log should not exist"); +                OMC_ASSERT(ascii_file_contains(proc->f_stdout, "test_stdout\n"), "output file did not contain test message"); +                remove(proc->f_stdout); +                break; +            case 2: +                OMC_ASSERT(proc->returncode == 0, "echo should not fail"); +                OMC_ASSERT(access(proc->f_stdout, F_OK) != 0, "stdout.log should not exist"); +                OMC_ASSERT(access(proc->f_stderr, F_OK) == 0, "stderr.log should exist"); +                OMC_ASSERT(ascii_file_contains(proc->f_stderr, "test_stderr\n"), "output file did not contain test message"); +                remove(proc->f_stderr); +                break; +            case 3: +                OMC_ASSERT(proc->returncode == 0, "echo should not fail"); +                OMC_ASSERT(access("stdouterr.log", F_OK) == 0, "stdouterr.log should exist"); +                OMC_ASSERT(access("stderr.log", F_OK) != 0, "stdout.log should not exist"); +                OMC_ASSERT(access("stderr.log", F_OK) != 0, "stderr.log should not exist"); +                OMC_ASSERT(ascii_file_contains(proc->f_stdout, "test_stdout\ntest_stderr\n"), "output file did not contain test message"); +                remove(proc->f_stdout); +                remove(proc->f_stderr); +                break; +            default: +                break; +        } +    } +} + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    OMC_TEST_FUNC *tests[] = { +        test_shell_output_null_args, +        test_shell_output_non_zero_exit, +        test_shell_output, +        test_shell_safe_verify_restrictions, +        test_shell_safe, +        test_shell_null_proc, +        test_shell_null_args, +        test_shell_non_zero_exit, +        test_shell_exit, +        test_shell, +    }; +    OMC_TEST_RUN(tests); +    OMC_TEST_END_MAIN(); +} diff --git a/tests/test_template.c b/tests/test_template.c new file mode 100644 index 0000000..fda860a --- /dev/null +++ b/tests/test_template.c @@ -0,0 +1,100 @@ +#include "testing.h" + +extern void tpl_reset(); +extern struct tpl_item *tpl_pool[]; +extern unsigned tpl_pool_used; +extern unsigned tpl_pool_func_used; + + +static int adder(struct tplfunc_frame *frame, void *result) { +    int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); +    int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); +    sprintf(result, "%d", a + b); +    return 0; +} + +static int subtractor(struct tplfunc_frame *frame, void *result) { +    int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); +    int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); +    sprintf(result, "%d", a - b); +    return 0; +} + +static int multiplier(struct tplfunc_frame *frame, void *result) { +    int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); +    int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); +    sprintf(result, "%d", a * b); +    return 0; +} + +static int divider(struct tplfunc_frame *frame, void *result) { +    int a = (int) strtol(frame->argv[0].t_char_ptr, NULL, 10); +    int b = (int) strtol(frame->argv[1].t_char_ptr, NULL, 10); +    sprintf(result, "%d", a / b); +    return 0; +} + +void test_tpl_workflow() { +    char *data = strdup("Hello world!"); +    tpl_reset(); +    tpl_register("hello_message", &data); + +    OMC_ASSERT(strcmp(tpl_render("I said, \"{{ hello_message }}\""), "I said, \"Hello world!\"") == 0, "stored value in key is incorrect"); +    setenv("HELLO", "Hello environment!", 1); +    OMC_ASSERT(strcmp(tpl_render("{{ env:HELLO }}"), "Hello environment!") == 0, "environment variable content mismatch"); +    unsetenv("HELLO"); +    guard_free(data); +} + +void test_tpl_register() { +    char *data = strdup("Hello world!"); +    tpl_reset(); +    unsigned used_before_register = tpl_pool_used; +    tpl_register("hello_message", &data); + +    OMC_ASSERT(tpl_pool_used == (used_before_register + 1), "tpl_register did not increment allocation counter"); +    OMC_ASSERT(tpl_pool[used_before_register] != NULL, "register did not allocate a tpl_item record in the pool"); +    free(data); +} + +void test_tpl_register_func() { +    tpl_reset(); +    struct tplfunc_frame tasks[] = { +            {.key = "add", .argc = 2, .func = adder}, +            {.key = "sub", .argc = 2, .func = subtractor}, +            {.key = "mul", .argc = 2, .func = multiplier}, +            {.key = "div", .argc = 2, .func = divider}, +    }; +    tpl_register_func("add", &tasks[0]); +    tpl_register_func("sub", &tasks[1]); +    tpl_register_func("mul", &tasks[2]); +    tpl_register_func("div", &tasks[3]); +    OMC_ASSERT(tpl_pool_func_used == sizeof(tasks) / sizeof(*tasks), "unexpected function pool used"); + +    char *result = NULL; +    result = tpl_render("{{ func:add(0,3) }}"); +    OMC_ASSERT(result != NULL && strcmp(result, "3") == 0, "Answer was not 3"); +    result = tpl_render("{{ func:add(1,2) }}"); +    OMC_ASSERT(result != NULL && strcmp(result, "3") == 0, "Answer was not 3"); +    result = tpl_render("{{ func:sub(6,3) }}"); +    OMC_ASSERT(result != NULL && strcmp(result, "3") == 0, "Answer was not 3"); +    result = tpl_render("{{ func:sub(4,1) }}"); +    OMC_ASSERT(result != NULL && strcmp(result, "3") == 0, "Answer was not 3"); +    result = tpl_render("{{ func:mul(1,   3) }}"); +    OMC_ASSERT(result != NULL && strcmp(result, "3") == 0, "Answer was not 3"); +    result = tpl_render("{{ func:div(6,2) }}"); +    OMC_ASSERT(result != NULL && strcmp(result, "3") == 0, "Answer was not 3"); +    result = tpl_render("{{ func:div(3,1) }}"); +    OMC_ASSERT(result != NULL && strcmp(result, "3") == 0, "Answer was not 3"); +} + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    OMC_TEST_FUNC *tests[] = { +        test_tpl_workflow, +        test_tpl_register_func, +        test_tpl_register, +    }; +    OMC_TEST_RUN(tests); +    OMC_TEST_END_MAIN(); +}
\ No newline at end of file diff --git a/tests/test_utils.c b/tests/test_utils.c new file mode 100644 index 0000000..a2fa8c6 --- /dev/null +++ b/tests/test_utils.c @@ -0,0 +1,479 @@ +#include "testing.h" + +char cwd_start[PATH_MAX]; +char cwd_workspace[PATH_MAX]; +extern char *dirstack[]; +extern const ssize_t dirstack_max; +extern ssize_t dirstack_len; + +void test_listdir() { +    const char *dirs[] = { +            "test_listdir", +            "test_listdir/1", +            "test_listdir/2", +            "test_listdir/3", +    }; +    for (size_t i = 0; i < sizeof(dirs) / sizeof(*dirs); i++) { +        mkdir(dirs[i], 0755); +    } +    struct StrList *listing; +    listing = listdir(dirs[0]); +    OMC_ASSERT(listing != NULL, "listdir failed"); +    OMC_ASSERT(listing && (strlist_count(listing) == (sizeof(dirs) / sizeof(*dirs)) - 1), "should have 3"); +    guard_strlist_free(&listing); +} + +void test_redact_sensitive() { +    const char *data[] = { +            "100 dollars!", +            "bananas apples pears", +            "have a safe trip", +    }; +    const char *to_redact[] = { +            "dollars", +            "bananas", +            "have a safe trip", +            NULL, +    }; +    const char *expected[] = { +            "100 ***REDACTED***!", +            "***REDACTED*** apples pears", +            "***REDACTED***", +    }; + +    for (size_t i = 0; i < sizeof(data) / sizeof(*data); i++) { +        char *input = strdup(data[i]); +        char output[100] = {0}; +        redact_sensitive(to_redact, sizeof(to_redact) / sizeof(*to_redact), input, output, sizeof(output) - 1); +        OMC_ASSERT(strcmp(output, expected[i]) == 0, "incorrect redaction"); +    } +} + +void test_fix_tox_conf() { +    const char *filename = "tox.ini"; +    const char *data = "[testenv]\n" +                       "key_before = 1\n" +                       "commands = pytest -sx tests\n" +                       "key_after = 2\n"; +    const char *expected = "{posargs}\n"; +    char *result = NULL; +    FILE *fp; + +    remove(filename); +    fp = fopen(filename, "w"); +    if (fp) { +        fprintf(fp, "%s", data); +        fclose(fp); +        OMC_ASSERT(fix_tox_conf(filename, &result) == 0, "fix_tox_conf failed"); +    } else { +        OMC_ASSERT(false, "writing mock tox.ini failed"); +    } + +    char **lines = file_readlines(result, 0, 0, NULL); +    OMC_ASSERT(strstr_array(lines, expected) != NULL, "{posargs} not found in result"); + +    remove(result); +    guard_free(result); +} + +void test_xml_pretty_print_in_place() { +    FILE *fp; +    const char *filename = "ugly.xml"; +    const char *data = "<things><abc>123</abc><abc>321</abc></things>"; +    const char *expected = "<?xml version=\"1.0\"?>\n" +                           "<things>\n" +                           "  <abc>123</abc>\n" +                           "  <abc>321</abc>\n" +                           "</things>\n"; + +    remove(filename); +    fp = fopen(filename, "w"); +    if (fp) { +        fprintf(fp, "%s", data); +        fclose(fp); +        OMC_ASSERT(xml_pretty_print_in_place( +                filename, +                OMC_XML_PRETTY_PRINT_PROG, +                OMC_XML_PRETTY_PRINT_ARGS) == 0, +                   "xml pretty print failed (xmllint not installed?)"); +    } else { +        OMC_ASSERT(false, "failed to create input file"); +    } + +    fp = fopen(filename, "r"); +    char buf[BUFSIZ] = {0}; +    if (fread(buf, sizeof(*buf), sizeof(buf) - 1, fp) < 1) { +        OMC_ASSERT(false, "failed to consume formatted xml file contents"); +    } +    OMC_ASSERT(strcmp(expected, buf) == 0, "xml file was not reformatted"); +} + +void test_path_store() { +    char *dest = NULL; +    chdir(cwd_workspace); +    int result = path_store(&dest, PATH_MAX, cwd_workspace, "test_path_store"); +    OMC_ASSERT(result == 0, "path_store encountered an error"); +    OMC_ASSERT(dest != NULL, "dest should not be NULL"); +    OMC_ASSERT(dest && (access(dest, F_OK) == 0), "destination path was not created"); +    rmtree(dest); +} + +void test_isempty_dir() { +    const char *dname = "test_isempty_dir"; +    rmtree(dname); +    mkdir(dname, 0755); +    OMC_ASSERT(isempty_dir(dname) > 0, "empty directory went undetected"); + +    char path[PATH_MAX]; +    sprintf(path, "%s/file.txt", dname); +    touch(path); + +    OMC_ASSERT(isempty_dir(dname) == 0, "populated directory went undetected"); +} + +void test_xmkstemp() { +    FILE *tempfp = NULL; +    char *tempfile; +    const char *data = "Hello, world!\n"; + +    tempfile = xmkstemp(&tempfp, "w"); +    OMC_ASSERT(tempfile != NULL, "failed to create temporary file"); +    fprintf(tempfp, "%s", data); +    fclose(tempfp); + +    char buf[100] = {0}; +    tempfp = fopen(tempfile, "r"); +    fgets(buf, sizeof(buf) - 1, tempfp); +    fclose(tempfp); + +    OMC_ASSERT(strcmp(buf, data) == 0, "data written to temp file is incorrect"); +    remove(tempfile); +    free(tempfile); +} + +void test_msg() { +    int flags_indent[] = { +        0, +        OMC_MSG_L1, +        OMC_MSG_L2, +        OMC_MSG_L3, +    }; +    int flags_info[] = { +        //OMC_MSG_NOP, +        OMC_MSG_SUCCESS, +        OMC_MSG_WARN, +        OMC_MSG_ERROR, +        //OMC_MSG_RESTRICT, +    }; +    for (size_t i = 0; i < sizeof(flags_indent) / sizeof(*flags_indent); i++) { +        for (size_t x = 0; x < sizeof(flags_info) / sizeof(*flags_info); x++) { +            msg(flags_info[x] | flags_indent[i], "Indent level %zu...Message %zu\n", i, x); +        } +    } +} + +void test_git_clone_and_describe() { +    struct Process proc; +    memset(&proc, 0, sizeof(proc)); +    const char *local_workspace = "test_git_clone"; +    const char *repo = "localrepo"; +    const char *repo_git = "localrepo.git"; +    char *cwd = getcwd(NULL, PATH_MAX); + +    // remove the bare repo, and local clone +    rmtree(repo); +    rmtree(repo_git); + +    mkdir(local_workspace, 0755); +    chdir(local_workspace); + +    // initialize a bare repo so we can clone it +    char init_cmd[PATH_MAX]; +    sprintf(init_cmd, "git init --bare %s", repo_git); +    system(init_cmd); + +    // clone the bare repo +    OMC_ASSERT(git_clone(&proc, repo_git, repo, NULL) == 0, +               "a local clone of bare repository should have succeeded"); +    chdir(repo); + +    // interact with it +    touch("README.md"); +    system("git config user.name omc"); +    system("git config user.email null@null.null"); +    system("git add README.md"); +    system("git commit --no-gpg-sign -m Test"); +    system("git push -u origin"); +    system("git --no-pager log"); + +    // test git_describe is functional +    char *taginfo_none = git_describe("."); +    OMC_ASSERT(taginfo_none != NULL, "should be a git hash, not NULL"); + +    system("git tag -a 1.0.0 -m Mock"); +    system("git push --tags origin"); +    char *taginfo = git_describe("."); +    OMC_ASSERT(taginfo != NULL, "should be 1.0.0, not NULL"); +    OMC_ASSERT(strcmp(taginfo, "1.0.0") == 0, "just-created tag was not described correctly"); +    chdir(".."); + +    char *taginfo_outer = git_describe(repo); +    OMC_ASSERT(taginfo_outer != NULL, "should be 1.0.0, not NULL"); +    OMC_ASSERT(strcmp(taginfo_outer, "1.0.0") == 0, "just-created tag was not described correctly (out-of-dir invocation)"); + +    char *taginfo_bad = git_describe("abc1234_not_here_or_there"); +    OMC_ASSERT(taginfo_bad == NULL, "a repository that shouldn't exist... exists and has a tag."); +    chdir(cwd); +} + +void test_touch() { +    OMC_ASSERT(touch("touchedfile.txt") == 0, "touch failed"); +    OMC_ASSERT(access("touchedfile.txt", F_OK) == 0, "touched file does not exist"); +    remove("touchedfile.txt"); +} + +void test_find_program() { +    OMC_ASSERT(find_program("willnotexist123") == NULL, "did not return NULL"); +    OMC_ASSERT(find_program("find") != NULL, "program not available (OS dependent)"); +} + +static int file_readlines_callback_modify(size_t size, char **line) { +    char *data = (*line); +    for (size_t i = 0; i < strlen(data); i++) { +        if (isalnum(data[i])) { +            data[i] = 'x'; +        } +    } +    return 0; +} + +static int file_readlines_callback_get_specific_line(size_t size, char **line) { +    char *data = (*line); +    for (size_t i = 0; i < strlen(data); i++) { +        if (!strcmp(data, "see?\n")) { +            return 0; +        } +    } +    return 1; +} + + +void test_file_readlines() { +    const char *filename = "file_readlines.txt"; +    const char *data = "I am\na file\nwith multiple lines\nsee?\n"; +    FILE *fp = fopen(filename, "w"); +    if (!fp) { +        perror(filename); +        return; +    } +    if (fwrite(data, sizeof(*data), strlen(data), fp) != strlen(data)) { +        perror("short write"); +        return; +    } +    fclose(fp); + +    char **result = NULL; +    result = file_readlines(filename, 0, 0, NULL); +    int i; +    for (i = 0; result[i] != NULL; i++); +    OMC_ASSERT(num_chars(data, '\n') == i, "incorrect number of lines in data"); +    OMC_ASSERT(strcmp(result[3], "see?\n") == 0, "last line in data is incorrect'"); +    GENERIC_ARRAY_FREE(result); + +    result = file_readlines(filename, 0, 0, file_readlines_callback_modify); +    OMC_ASSERT(strcmp(result[3], "xxx?\n") == 0, "last line should be: 'xxx?\\n'"); +    GENERIC_ARRAY_FREE(result); + +    result = file_readlines(filename, 0, 0, file_readlines_callback_get_specific_line); +    OMC_ASSERT(strcmp(result[0], "see?\n") == 0, "the first record of the result is not the last line of the file 'see?\\n'"); +    GENERIC_ARRAY_FREE(result); +    remove(filename); +} + +void test_path_dirname() { +    const char *data[] = { +            "a/b/c", "a/b", +            "This/is/a/test", "This/is/a", +    }; +    for (size_t i = 0; i < sizeof(data) / sizeof(*data); i += 2) { +        const char *input = data[i]; +        const char *expected = data[i + 1]; +        char tmp[PATH_MAX] = {0}; +        strcpy(tmp, input); + +        char *result = path_dirname(tmp); +        OMC_ASSERT(strcmp(expected, result) == 0, NULL); +    } +} + +void test_path_basename() { +    const char *data[] = { +        "a/b/c", "c", +        "This/is/a/test", "test", +    }; +    for (size_t i = 0; i < sizeof(data) / sizeof(*data); i += 2) { +        const char *input = data[i]; +        const char *expected = data[i + 1]; +        char *result = path_basename(input); +        OMC_ASSERT(strcmp(expected, result) == 0, NULL); +    } +} + +void test_expandpath() { +    char *home; + +    const char *homes[] = { +            "HOME", +            "USERPROFILE", +    }; +    for (size_t i = 0; i < sizeof(homes) / sizeof(*homes); i++) { +        home = getenv(homes[i]); +        if (home) { +            break; +        } +    } + +    char path[PATH_MAX] = {0}; +    strcat(path, "~"); +    strcat(path, DIR_SEP); + +    char *expanded = expandpath(path); +    OMC_ASSERT(startswith(expanded, home) > 0, expanded); +    OMC_ASSERT(endswith(expanded, DIR_SEP) > 0, "the input string ends with a directory separator, the result did not"); +    free(expanded); +} + +void test_rmtree() { +    const char *root = "rmtree_dir"; +    const char *tree[] = { +            "abc", +            "123", +            "cba", +            "321" +    }; +    chdir(cwd_workspace); + +    mkdir(root, 0755); +    for (size_t i = 0; i < sizeof(tree) / sizeof(*tree); i++) { +        char path[PATH_MAX]; +        char mockfile[PATH_MAX + 10]; +        sprintf(path, "%s/%s", root, tree[i]); +        sprintf(mockfile, "%s/%zu.txt", path, i); +        if (mkdir(path, 0755)) { +            perror(path); +            OMC_ASSERT(false, NULL); +        } +        touch(mockfile); +    } +    OMC_ASSERT(rmtree(root) == 0, "rmtree should have been able to remove the directory"); +    OMC_ASSERT(access(root, F_OK) < 0, "the directory is still present"); +} + +void test_dirstack() { +    const char *data[] = { +        "testdir", +        "1", +        "2", +        "3", +    }; + +    char cwd[PATH_MAX]; +    getcwd(cwd, PATH_MAX); +    for (size_t i = 0; i < sizeof(data) / sizeof(*data); i++) { +        mkdir(data[i], 0755); +        pushd(data[i]); +        getcwd(cwd, PATH_MAX); +    } +    OMC_ASSERT(dirstack_len == sizeof(data) / sizeof(*data), NULL); +    OMC_ASSERT(strcmp(dirstack[0], cwd_workspace) == 0, NULL); + +    for (int i = 1; i < dirstack_len; i++) { +        OMC_ASSERT(endswith(dirstack[i], data[i - 1]), NULL); +    } + +    for (size_t i = 0, x = dirstack_len - 1; x != 0 && i < sizeof(data) / sizeof(*data); i++, x--) { +        char *expected = strdup(dirstack[x]); +        popd(); +        getcwd(cwd, PATH_MAX); +        OMC_ASSERT(strcmp(cwd, expected) == 0, NULL); +        free(expected); +    } +} + +void test_pushd_popd() { +    const char *dname = "testdir"; +    chdir(cwd_workspace); +    rmtree(dname); + +    OMC_ASSERT(mkdir(dname, 0755) == 0, "directory should not exist yet"); +    OMC_ASSERT(pushd(dname) == 0, "failed to enter directory"); +    char *cwd = getcwd(NULL, PATH_MAX); + +    // we should be inside the test directory, not the starting directory +    OMC_ASSERT(strcmp(cwd_workspace, cwd) != 0, ""); +    OMC_ASSERT(popd() == 0, "return from directory failed"); + +    char *cwd_after_popd = getcwd(NULL, PATH_MAX); +    OMC_ASSERT(strcmp(cwd_workspace, cwd_after_popd) == 0, "should be the path where we started"); +} + +void test_pushd_popd_suggested_workflow() { +    const char *dname = "testdir"; +    chdir(cwd_workspace); +    rmtree(dname); + +    remove(dname); +    if (!mkdir(dname, 0755)) { +        if (!pushd(dname)) { +            char *cwd = getcwd(NULL, PATH_MAX); +            OMC_ASSERT(strcmp(cwd_workspace, cwd) != 0, NULL); +            // return from dir +            popd(); +            free(cwd); +        } +        // cwd should be our starting directory +        char *cwd = getcwd(NULL, PATH_MAX); +        OMC_ASSERT(strcmp(cwd_workspace, cwd) == 0, NULL); +    } else { +        OMC_ASSERT(false, "mkdir function failed"); +    } +} + + +int main(int argc, char *argv[]) { +    OMC_TEST_BEGIN_MAIN(); +    OMC_TEST_FUNC *tests[] = { +            test_listdir, +            test_redact_sensitive, +            test_fix_tox_conf, +            test_xml_pretty_print_in_place, +            test_path_store, +            test_isempty_dir, +            test_xmkstemp, +            test_msg, +            test_git_clone_and_describe, +            test_touch, +            test_find_program, +            test_file_readlines, +            test_path_dirname, +            test_path_basename, +            test_expandpath, +            test_rmtree, +            test_dirstack, +            test_pushd_popd, +            test_pushd_popd_suggested_workflow, +    }; +    const char *ws = "workspace"; +    getcwd(cwd_start, sizeof(cwd_start) - 1); +    mkdir(ws, 0755); +    chdir(ws); +    getcwd(cwd_workspace, sizeof(cwd_workspace) - 1); + +    OMC_TEST_RUN(tests); + +    chdir(cwd_start); +    if (rmtree(cwd_workspace)) { +        perror(cwd_workspace); +    } +    OMC_TEST_END_MAIN(); +}
\ No newline at end of file diff --git a/tests/testing.h b/tests/testing.h new file mode 100644 index 0000000..35bfbd2 --- /dev/null +++ b/tests/testing.h @@ -0,0 +1,174 @@ +#ifndef OMC_TESTING_H +#define OMC_TESTING_H +#include "omc.h" +#define OMC_TEST_RUN_MAX 10000 +#define OMC_TEST_SUITE_FATAL 1 +#define OMC_TEST_SUITE_SKIP 127 + +#ifndef __FILE_NAME__ +#define __FILE_NAME__ __FILE__ +#endif + +typedef void(OMC_TEST_FUNC)(); +struct omc_test_result_t { +    const char *filename; +    const char *funcname; +    int lineno; +    const char *msg_assertion; +    const char *msg_reason; +    const int status; +    const int skip; +} omc_test_results[OMC_TEST_RUN_MAX]; +size_t omc_test_results_i = 0; + +void omc_testing_record_result(struct omc_test_result_t result); + +void omc_testing_record_result(struct omc_test_result_t result) { +    memcpy(&omc_test_results[omc_test_results_i], &result, sizeof(result)); +    omc_test_results_i++; +} + +int omc_testing_has_failed() { +    for (size_t i = 0; i < omc_test_results_i; i++) { +        if (omc_test_results[i].status == false) { +            return 1; +        } +    } +    return 0; +} +void omc_testing_record_result_summary() { +    size_t failed = 0; +    size_t skipped = 0; +    size_t passed = 0; +    int do_message; +    static char status_msg[255] = {0}; +    for (size_t i = 0; i < omc_test_results_i; i++) { +        if (omc_test_results[i].status && omc_test_results[i].skip) { +            strcpy(status_msg, "SKIP"); +            do_message = 1; +            skipped++; +        } else if (!omc_test_results[i].status) { +            strcpy(status_msg, "FAIL"); +            do_message = 1; +            failed++; +        } else { +            strcpy(status_msg, "PASS"); +            do_message = 0; +            passed++; +        } +        fprintf(stdout, "[%s] %s:%d :: %s() => %s", +               status_msg, +               omc_test_results[i].filename, +               omc_test_results[i].lineno, +               omc_test_results[i].funcname, +               omc_test_results[i].msg_assertion); +        if (do_message) { +            fprintf(stdout, "\n      \\_ %s", omc_test_results[i].msg_reason); +        } +        fprintf(stdout, "\n"); +    } +    fprintf(stdout, "\n[UNIT] %zu tests passed, %zu tests failed, %zu tests skipped out of %zu\n", passed, failed, skipped, omc_test_results_i); +} + +char *omc_testing_read_ascii(const char *filename) { +    struct stat st; +    if (stat(filename, &st)) { +        perror(filename); +        return NULL; +    } + +    FILE *fp; +    fp = fopen(filename, "r"); +    if (!fp) { +        perror(filename); +        return NULL; +    } + +    char *result; +    result = calloc(st.st_size + 1, sizeof(*result)); +    if (fread(result, sizeof(*result), st.st_size, fp) < 1) { +        perror(filename); +        fclose(fp); +        return NULL; +    } + +    fclose(fp); +    return result; +} + +int omc_testing_write_ascii(const char *filename, const char *data) { +    FILE *fp; +    fp = fopen(filename, "w+"); +    if (!fp) { +        perror(filename); +        return -1; +    } + +    if (!fprintf(fp, "%s", data)) { +        perror(filename); +        fclose(fp); +        return -1; +    } + +    fclose(fp); +    return 0; +} + +#define OMC_TEST_BEGIN_MAIN() do { \ +        setenv("PYTHONUNBUFFERED", "1", 1); \ +        fflush(stdout); \ +        fflush(stderr); \ +        setvbuf(stdout, NULL, _IONBF, 0); \ +        setvbuf(stderr, NULL, _IONBF, 0); \ +        atexit(omc_testing_record_result_summary); \ +    } while (0) +#define OMC_TEST_END_MAIN() do { return omc_testing_has_failed(); } while (0) + +#define OMC_ASSERT(COND, REASON) do { \ +        omc_testing_record_result((struct omc_test_result_t) { \ +            .filename = __FILE_NAME__, \ +            .funcname = __FUNCTION__,  \ +            .lineno = __LINE__,        \ +            .status = (COND),             \ +            .msg_assertion = "ASSERT(" #COND ")",                 \ +            .msg_reason = REASON } );  \ +    } while (0) + +#define OMC_ASSERT_FATAL(COND, REASON) do { \ +        omc_testing_record_result((struct omc_test_result_t) { \ +            .filename = __FILE_NAME__, \ +            .funcname = __FUNCTION__,  \ +            .lineno = __LINE__,        \ +            .status = (COND),             \ +            .msg_assertion = "ASSERT FATAL (" #COND ")",                 \ +            .msg_reason = REASON }       \ +        );    \ +        if (omc_test_results[omc_test_results_i ? omc_test_results_i - 1 : omc_test_results_i].status == false) {\ +            exit(OMC_TEST_SUITE_FATAL); \ +        } \ +    } while (0) + +#define OMC_SKIP_IF(COND, REASON) do { \ +        omc_testing_record_result((struct omc_test_result_t) { \ +            .filename = __FILE_NAME__, \ +            .funcname = __FUNCTION__,  \ +            .lineno = __LINE__,        \ +            .status = true, \ +            .skip = (COND), \ +            .msg_assertion = "SKIP (" #COND ")",                 \ +            .msg_reason = REASON }       \ +        );    \ +        if (omc_test_results[omc_test_results_i ? omc_test_results_i - 1 : omc_test_results_i].skip == true) {\ +            return; \ +        } \ +    } while (0) + +#define OMC_TEST_RUN(X) do { \ +        for (size_t i = 0; i < sizeof(X) / sizeof(*X); i++) { \ +            if (X[i]) { \ +                X[i](); \ +            } \ +        } \ +    } while (0) + +#endif //OMC_TESTING_H | 
