aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@gmail.com>2024-06-09 08:09:49 -0400
committerJoseph Hunkeler <jhunkeler@gmail.com>2024-06-09 08:09:49 -0400
commit3357f87a5efe478be2433787c29de640dc21b33d (patch)
tree262c5694ce650506afb348e7cdc521c7cb9cc629
parent11aa1d44d95da221073e512fbec3bbccc0f1a46b (diff)
downloadstasis-3357f87a5efe478be2433787c29de640dc21b33d.tar.gz
Initial commit of unit tests [WIP]
-rw-r--r--CMakeLists.txt10
-rw-r--r--tests/CMakeLists.txt28
-rw-r--r--tests/test_docker.c71
-rw-r--r--tests/test_relocation.c62
-rw-r--r--tests/test_str.c379
-rw-r--r--tests/test_strlist.c613
-rw-r--r--tests/test_system.c145
-rw-r--r--tests/test_template.c100
-rw-r--r--tests/test_utils.c477
-rw-r--r--tests/testing.h170
10 files changed, 2055 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0824baa..66fa91b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,6 +23,16 @@ endif()
add_subdirectory(src)
+option(BUILD_TESTING_DEBUG OFF)
+if (BUILD_TESTING_DEBUG)
+ add_compile_options(DEBUG)
+endif()
+option(BUILD_TESTING OFF)
+if (BUILD_TESTING)
+ enable_testing()
+ add_subdirectory(tests)
+endif()
+
set(SYSCONFDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_SYSCONFDIR}")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/include/config.h @ONLY)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/omc.ini DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/omc)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 0000000..dd349e2
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,28 @@
+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)
+
+file(GLOB files "test_*.c")
+
+foreach(file ${files})
+ string(REGEX REPLACE "(^.*/|\\.[^.]*$)" "" file_without_ext ${file})
+ add_executable(${file_without_ext} ${file})
+ if(NOT(MSVC))
+ target_compile_options(${file_without_ext} PRIVATE
+ -Wno-unused-parameter
+ -Wno-unused-but-set-variable
+ -Wno-incompatible-pointer-types-discards-qualifiers
+ -Wno-discarded-qualifiers)
+ 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..223de2a
--- /dev/null
+++ b/tests/test_docker.c
@@ -0,0 +1,71 @@
+#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() {
+ 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(".", "--arch $(uname -m) -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_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..2321ccd
--- /dev/null
+++ b/tests/test_str.c
@@ -0,0 +1,379 @@
+#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 = -1},
+ {.data = "I have a pencil box", .input = NULL, .expected = -1},
+ {.data = NULL, .input = NULL, .expected = -1},
+ };
+ 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 = -1},
+ {.data = "I have a pencil box", .input = NULL, .expected = -1},
+ {.data = NULL, .input = NULL, .expected = -1},
+ };
+ 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);
+ printf("data = '%s', delim = '%s', expected = '%s', result = '%s'\n", tc[i].data, tc[i].delim, tc[i].expected, result);
+ 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);
+ printf("result = '%s', expected = '%s'\n", result, tc[i].expected);
+ 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..6111013
--- /dev/null
+++ b/tests/test_strlist.c
@@ -0,0 +1,613 @@
+#include "testing.h"
+
+#define BOILERPLATE_TEST_STRLIST_AS_TYPE(FUNC, TYPE) \
+do { \
+ struct StrList *list; \
+ list = strlist_init(); \
+ puts("----"); \
+ 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",
+ };
+ 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; i < sizeof(expected) / sizeof(*expected); 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();
+ 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"
+ };
+ const char *expected[] = {
+ "d", "c", "b", "a"
+ };
+ 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"
+ };
+ const char *expected[] = {
+ "a", "b", "c", "d"
+ };
+ 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"
+ };
+ const char *expected[] = {
+ "a", "bb", "ccc", "dddd"
+ };
+ 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"
+ };
+ const char *expected[] = {
+ "dddd", "ccc", "bb", "a"
+ };
+ 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..cada76d
--- /dev/null
+++ b/tests/test_utils.c
@@ -0,0 +1,477 @@
+#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, 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 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..2b5c1ba
--- /dev/null
+++ b/tests/testing.h
@@ -0,0 +1,170 @@
+#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
+
+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