aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@users.noreply.github.com>2024-06-20 15:10:56 -0400
committerGitHub <noreply@github.com>2024-06-20 15:10:56 -0400
commit931ee28eb9c5b5e3c2b0d3008f5f65d810dc9b0c (patch)
tree5dbcccffd509fa71a99c351ed4628ed0841e1e46
parent11aa1d44d95da221073e512fbec3bbccc0f1a46b (diff)
downloadohmycal-master.tar.gz
Unit tests (#6)HEADmaster
* 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
-rw-r--r--.github/workflows/cmake-multi-platform.yml12
-rw-r--r--CMakeLists.txt10
-rw-r--r--include/ini.h2
-rw-r--r--include/str.h31
-rw-r--r--include/strlist.h8
-rw-r--r--include/system.h2
-rw-r--r--include/template.h2
-rw-r--r--include/utils.h2
-rw-r--r--src/artifactory.c15
-rw-r--r--src/docker.c4
-rw-r--r--src/ini.c2
-rw-r--r--src/str.c154
-rw-r--r--src/strlist.c239
-rw-r--r--src/system.c54
-rw-r--r--src/template.c54
-rw-r--r--src/utils.c38
-rw-r--r--tests/CMakeLists.txt32
-rw-r--r--tests/test_docker.c73
-rw-r--r--tests/test_ini.c108
-rw-r--r--tests/test_relocation.c62
-rw-r--r--tests/test_str.c377
-rw-r--r--tests/test_strlist.c617
-rw-r--r--tests/test_system.c145
-rw-r--r--tests/test_template.c100
-rw-r--r--tests/test_utils.c479
-rw-r--r--tests/testing.h174
26 files changed, 2560 insertions, 236 deletions
diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml
index 61ed38b..6d4709a 100644
--- a/.github/workflows/cmake-multi-platform.yml
+++ b/.github/workflows/cmake-multi-platform.yml
@@ -29,7 +29,7 @@ jobs:
c_compiler: gcc
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set reusable strings
id: strings
@@ -40,19 +40,19 @@ jobs:
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
run: >
- sudo apt install -y libcurl4-openssl-dev libxml2-dev rsync
+ sudo apt install -y libcurl4-openssl-dev libxml2-dev libxml2-utils rsync
- name: Configure CMake
run: >
cmake -B ${{ steps.strings.outputs.build-output-dir }}
-DCMAKE_C_COMPILER=${{ matrix.c_compiler }}
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
+ -DBUILD_TESTING=ON
-S ${{ github.workspace }}
- name: Build
run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }}
- # TODO: Write ctests
- #- name: Test
- # working-directory: ${{ steps.strings.outputs.build-output-dir }}
- # run: ctest --build-config ${{ matrix.build_type }}
+ - name: Test
+ working-directory: ${{ steps.strings.outputs.build-output-dir }}
+ run: ctest -V --build-config ${{ matrix.build_type }} --output-junit results.xml --test-output-size-passed 65536 --test-output-size-failed 65536
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0824baa..ca10017 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(-DDEBUG)
+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/include/ini.h b/include/ini.h
index fa2bd98..022a066 100644
--- a/include/ini.h
+++ b/include/ini.h
@@ -98,7 +98,7 @@ struct INIFILE *ini_open(const char *filename);
* @param value
* @return
*/
-struct INISection *ini_section_search(struct INIFILE **ini, unsigned mode, char *value);
+struct INISection *ini_section_search(struct INIFILE **ini, unsigned mode, const char *value);
/**
*
diff --git a/include/str.h b/include/str.h
index 8018cc0..595a055 100644
--- a/include/str.h
+++ b/include/str.h
@@ -29,7 +29,7 @@ int num_chars(const char *sptr, int ch);
*
* @param sptr string to scan
* @param pattern string to search for
- * @return 1 = found, 0 = not found, -1 = error
+ * @return 1 = found, 0 = not found / error
*/
int startswith(const char *sptr, const char *pattern);
@@ -38,7 +38,7 @@ int startswith(const char *sptr, const char *pattern);
*
* @param sptr string to scan
* @param pattern string to search for
- * @return 1 = found, 0 = not found, -1 = error
+ * @return 1 = found, 0 = not found / error
*/
int endswith(const char *sptr, const char *pattern);
@@ -51,33 +51,6 @@ int endswith(const char *sptr, const char *pattern);
void strchrdel(char *sptr, const char *chars);
/**
- * Find the integer offset of the first occurrence of `ch` in `sptr`
- *
- * ~~~{.c}
- * char buffer[255];
- * char string[] = "abc=123";
- * long int separator_offset = strchroff(string, '=');
- * for (long int i = 0; i < separator_offset); i++) {
- * buffer[i] = string[i];
- * }
- * ~~~
- *
- * @param sptr string to scan
- * @param ch character to find
- * @return offset to character in string, or 0 on failure
- */
-long int strchroff(const char *sptr, int ch);
-
-/**
- * This function scans `sptr` from right to left removing any matches to `suffix`
- * from the string.
- *
- * @param sptr string to be modified
- * @param suffix string to be removed from `sptr`
- */
-void strdelsuffix(char *sptr, const char *suffix);
-
-/**
* Split a string by every delimiter in `delim` string.
*
* Callee should free memory using `GENERIC_ARRAY_FREE()`
diff --git a/include/strlist.h b/include/strlist.h
index 2d3c3cf..3f35e23 100644
--- a/include/strlist.h
+++ b/include/strlist.h
@@ -44,4 +44,12 @@ struct StrList *strlist_copy(struct StrList *pStrList);
int strlist_cmp(struct StrList *a, struct StrList *b);
void strlist_free(struct StrList **pStrList);
+#define STRLIST_E_SUCCESS 0
+#define STRLIST_E_OUT_OF_RANGE 1
+#define STRLIST_E_INVALID_VALUE 2
+#define STRLIST_E_UNKNOWN 3
+extern int strlist_errno;
+const char *strlist_get_error(int flag);
+
+
#endif //OMC_STRLIST_H
diff --git a/include/system.h b/include/system.h
index 7428355..94d5a36 100644
--- a/include/system.h
+++ b/include/system.h
@@ -14,6 +14,8 @@
#include <sys/wait.h>
#include <sys/stat.h>
+#define OMC_SHELL_SAFE_RESTRICT ";&|()"
+
struct Process {
// Write stdout stream to file
char f_stdout[PATH_MAX];
diff --git a/include/template.h b/include/template.h
index a242a08..362eb3d 100644
--- a/include/template.h
+++ b/include/template.h
@@ -42,7 +42,7 @@ int tpl_render_to_file(char *str, const char *filename);
struct tplfunc_frame *tpl_getfunc(char *key);
struct tplfunc_frame;
-typedef int tplfunc(struct tplfunc_frame *frame);
+typedef int tplfunc(struct tplfunc_frame *frame, void *result);
struct tplfunc_frame {
char *key;
tplfunc *func;
diff --git a/include/utils.h b/include/utils.h
index a340cd7..8840a0d 100644
--- a/include/utils.h
+++ b/include/utils.h
@@ -329,7 +329,7 @@ char *collapse_whitespace(char **s);
* @param maxlen maximum length of dest byte array
* @return 0 on success, -1 on error
*/
-int redact_sensitive(const char **to_redact, char *src, char *dest, size_t maxlen);
+int redact_sensitive(const char **to_redact, size_t to_redact_size, char *src, char *dest, size_t maxlen);
/**
* Given a directory path, return a list of files
diff --git a/src/artifactory.c b/src/artifactory.c
index 4772602..5678d64 100644
--- a/src/artifactory.c
+++ b/src/artifactory.c
@@ -229,16 +229,15 @@ int jfrog_cli(struct JFRT_Auth *auth, char *args) {
}
const char *redactable[] = {
- "--access-token=",
- "--ssh-key-path=",
- "--ssh-passphrase=",
- "--client-cert-key-path=",
- "--client-cert-path=",
- "--password=",
- NULL,
+ auth->access_token,
+ auth->ssh_key_path,
+ auth->ssh_passphrase,
+ auth->client_cert_key_path,
+ auth->client_cert_path,
+ auth->password,
};
snprintf(cmd, sizeof(cmd) - 1, "jf %s %s", args, auth_args);
- redact_sensitive(redactable, cmd, cmd_redacted, sizeof(cmd_redacted) - 1);
+ redact_sensitive(redactable, sizeof(redactable) / sizeof (*redactable), cmd, cmd_redacted, sizeof(cmd_redacted) - 1);
guard_free(auth_args);
guard_strlist_free(&arg_map);
diff --git a/src/docker.c b/src/docker.c
index f8c9c11..308fbc7 100644
--- a/src/docker.c
+++ b/src/docker.c
@@ -28,7 +28,7 @@ int docker_script(const char *image, char *data, unsigned flags) {
char buffer[OMC_BUFSIZ];
memset(cmd, 0, sizeof(cmd));
- snprintf(cmd, sizeof(cmd) - 1, "docker run --rm -i %s /bin/bash -", image);
+ snprintf(cmd, sizeof(cmd) - 1, "docker run --rm -i %s /bin/sh -", image);
outfile = popen(cmd, "w");
if (!outfile) {
@@ -103,7 +103,7 @@ static int docker_exists() {
return false;
}
-char *docker_ident() {
+static char *docker_ident() {
FILE *fp = NULL;
char *tempfile = NULL;
char line[PATH_MAX];
diff --git a/src/ini.c b/src/ini.c
index 2d2eb57..decd29f 100644
--- a/src/ini.c
+++ b/src/ini.c
@@ -16,7 +16,7 @@ void ini_section_init(struct INIFILE **ini) {
(*ini)->section = calloc((*ini)->section_count + 1, sizeof(**(*ini)->section));
}
-struct INISection *ini_section_search(struct INIFILE **ini, unsigned mode, char *value) {
+struct INISection *ini_section_search(struct INIFILE **ini, unsigned mode, const char *value) {
struct INISection *result = NULL;
for (size_t i = 0; i < (*ini)->section_count; i++) {
if ((*ini)->section[i]->key != NULL) {
diff --git a/src/str.c b/src/str.c
index a5d42a6..16088b6 100644
--- a/src/str.c
+++ b/src/str.c
@@ -16,7 +16,7 @@ int num_chars(const char *sptr, int ch) {
int startswith(const char *sptr, const char *pattern) {
if (!sptr || !pattern) {
- return -1;
+ return 0;
}
for (size_t i = 0; i < strlen(pattern); i++) {
if (sptr[i] != pattern[i]) {
@@ -26,10 +26,9 @@ int startswith(const char *sptr, const char *pattern) {
return 1;
}
-
int endswith(const char *sptr, const char *pattern) {
if (!sptr || !pattern) {
- return -1;
+ return 0;
}
ssize_t sptr_size = (ssize_t) strlen(sptr);
ssize_t pattern_size = (ssize_t) strlen(pattern);
@@ -61,64 +60,10 @@ void strchrdel(char *sptr, const char *chars) {
return;
}
- while (*sptr != '\0') {
- for (int i = 0; chars[i] != '\0'; i++) {
- if (*sptr == chars[i]) {
- memmove(sptr, sptr + 1, strlen(sptr));
- }
- }
- sptr++;
- }
-}
-
-
-long int strchroff(const char *sptr, int ch) {
- char *orig = strdup(sptr);
- char *tmp = orig;
- long int result = 0;
-
- int found = 0;
- size_t i = 0;
-
- while (*tmp != '\0') {
- if (*tmp == ch) {
- found = 1;
- break;
- }
- tmp++;
- i++;
- }
-
- if (found == 0 && i == strlen(sptr)) {
- return -1;
- }
-
- result = tmp - orig;
- guard_free(orig);
-
- return result;
-}
-
-void strdelsuffix(char *sptr, const char *suffix) {
- if (!sptr || !suffix) {
- return;
- }
- size_t sptr_len = strlen(sptr);
- size_t suffix_len = strlen(suffix);
- intptr_t target_offset = sptr_len - suffix_len;
-
- // Prevent access to memory below input string
- if (target_offset < 0) {
- return;
- }
-
- // Create a pointer to
- char *target = sptr + target_offset;
- if (!strcmp(target, suffix)) {
- // Purge the suffix
- memset(target, '\0', suffix_len);
- // Recursive call continues removing suffix until it is gone
- strip(sptr);
+ for (size_t i = 0; i < strlen(chars); i++) {
+ char ch[2] = {0};
+ strncpy(ch, &chars[i], 1);
+ replace_text(sptr, ch, "", 0);
}
}
@@ -141,7 +86,7 @@ char** split(char *_sptr, const char* delim, size_t max)
if (max && i > max) {
break;
}
- split_alloc = num_chars(sptr, delim[i]);
+ split_alloc += num_chars(sptr, delim[i]);
}
// Preallocate enough records based on the number of delimiters
@@ -158,27 +103,35 @@ char** split(char *_sptr, const char* delim, size_t max)
}
// Separate the string into individual parts and store them in the result array
- int i = 0;
- size_t x = max;
char *token = NULL;
char *sptr_tmp = sptr;
- while((token = strsep(&sptr_tmp, delim)) != NULL) {
- result[i] = calloc(OMC_BUFSIZ, sizeof(char));
- if (x < max) {
- strcat(result[i], strstr(orig, delim) + 1);
+ size_t pos = 0;
+ size_t i;
+ for (i = 0; (token = strsep(&sptr_tmp, delim)) != NULL; i++) {
+ // When max is zero, record all tokens
+ if (max > 0 && i == max) {
+ // Maximum number of splits occurred.
+ // Record position in string
+ pos = token - sptr;
break;
- } else {
- if (!result[i]) {
- return NULL;
- }
- strcpy(result[i], token);
}
- i++;
- if (x > 0) {
- --x;
+ result[i] = calloc(OMC_BUFSIZ, sizeof(char));
+ if (!result[i]) {
+ return NULL;
}
- //memcpy(result[i], token, strlen(token) + 1); // copy the string contents into the record
+ strcpy(result[i], token);
}
+
+ // pos is non-zero when maximum split is reached
+ if (pos) {
+ // append the remaining string contents to array
+ result[i] = calloc(OMC_BUFSIZ, sizeof(char));
+ if (!result[i]) {
+ return NULL;
+ }
+ strcpy(result[i], &orig[pos]);
+ }
+
guard_free(sptr);
return result;
}
@@ -244,8 +197,9 @@ char *join_ex(char *separator, ...) {
if (tmp == NULL) {
perror("join_ex realloc failed");
return NULL;
+ } else if (tmp != argv) {
+ argv = tmp;
}
- argv = tmp;
size += strlen(current) + separator_len;
argv[argc] = strdup(current);
}
@@ -275,43 +229,37 @@ char *substring_between(char *sptr, const char *delims) {
// Ensure we have enough delimiters to continue
size_t delim_count = strlen(delims);
- if (delim_count != 2) {
+ if (delim_count < 2 || delim_count % 2) {
return NULL;
}
+ char delim_open[255] = {0};
+ strncpy(delim_open, delims, delim_count / 2);
+
+ char delim_close[255] = {0};
+ strcpy(delim_close, &delims[delim_count / 2]);
+
// Create pointers to the delimiters
- char *start = strchr(sptr, delims[0]);
+ char *start = strstr(sptr, delim_open);
if (start == NULL || strlen(start) == 0) {
return NULL;
}
- char *end = strchr(start + 1, delims[1]);
+ char *end = strstr(start + 1, delim_close);
if (end == NULL) {
return NULL;
}
- start++; // ignore leading delimiter
+ start += delim_count / 2; // ignore leading delimiter
// Get length of the substring
- size_t length = strlen(start);
+ size_t length = strlen(start) - strlen(end);
if (!length) {
return NULL;
}
- char *result = (char *)calloc(length + 1, sizeof(char));
- if (!result) {
- return NULL;
- }
-
- // Copy the contents of the substring to the result
- char *tmp = result;
- while (start != end) {
- *tmp = *start;
- tmp++;
- start++;
- }
-
- return result;
+ // Return the contents of the substring
+ return strndup(start, length);
}
/*
@@ -476,19 +424,16 @@ char *strip(char *sptr) {
size_t len = strlen(sptr);
if (len == 0) {
return sptr;
- }
- else if (len == 1 && (isblank(*sptr) || isspace(*sptr))) {
+ } else if (len == 1 && (isblank(*sptr) || isspace(*sptr))) {
*sptr = '\0';
return sptr;
- }
- for (size_t i = len; i != 0; --i) {
+ } for (size_t i = len; i != 0; --i) {
if (sptr[i] == '\0') {
continue;
}
if (isspace(sptr[i]) || isblank(sptr[i])) {
sptr[i] = '\0';
- }
- else {
+ } else {
break;
}
}
@@ -665,6 +610,9 @@ int strcmp_array(const char **a, const char **b) {
}
int isdigit_s(const char *s) {
+ if (!s || !strlen(s)) {
+ return 0; // nothing to do, fail
+ }
for (size_t i = 0; s[i] != '\0'; i++) {
if (isdigit(s[i]) == 0) {
return 0; // non-digit found, fail
diff --git a/src/strlist.c b/src/strlist.c
index d54ed06..bdacb5a 100644
--- a/src/strlist.c
+++ b/src/strlist.c
@@ -3,7 +3,6 @@
* @file strlist.c
*/
#include "strlist.h"
-//#include "url.h"
#include "utils.h"
/**
@@ -71,6 +70,7 @@ int strlist_append_file(struct StrList *pStrList, char *_path, ReaderFn *readerF
char *path = NULL;
char *filename = NULL;
char **data = NULL;
+ int is_url = strstr(_path, "://") != NULL;
if (readerFn == NULL) {
readerFn = reader_strlist_append_file;
@@ -78,17 +78,31 @@ int strlist_append_file(struct StrList *pStrList, char *_path, ReaderFn *readerF
path = strdup(_path);
if (path == NULL) {
-
retval = -1;
goto fatal;
}
- filename = expandpath(path);
-
- if (filename == NULL) {
-
- retval = -1;
- goto fatal;
+ if (is_url) {
+ int fd;
+ char tempfile[PATH_MAX] = {0};
+ strcpy(tempfile, "/tmp/.remote_file.XXXXXX");
+ if ((fd = mkstemp(tempfile)) < 0) {
+ retval = -1;
+ goto fatal;
+ }
+ close(fd);
+ filename = strdup(tempfile);
+ long http_code = download(path, filename, NULL);
+ if (HTTP_ERROR(http_code)) {
+ retval = -1;
+ goto fatal;
+ }
+ } else {
+ filename = expandpath(path);
+ if (filename == NULL) {
+ retval = -1;
+ goto fatal;
+ }
}
data = file_readlines(filename, 0, 0, readerFn);
@@ -96,17 +110,18 @@ int strlist_append_file(struct StrList *pStrList, char *_path, ReaderFn *readerF
retval = 1;
goto fatal;
}
-
for (size_t record = 0; data[record] != NULL; record++) {
strlist_append(&pStrList, data[record]);
guard_free(data[record]);
}
+ if (is_url) {
+ // remove temporary data
+ remove(filename);
+ }
guard_free(data);
fatal:
- if (filename != NULL) {
- guard_free(filename);
- }
+ guard_free(filename);
if (path != NULL) {
guard_free(path);
}
@@ -115,7 +130,7 @@ fatal:
}
/**
- * Append the contents of a `StrList` to another `StrList`
+ * Append the contents of `pStrList2` to `pStrList1`
* @param pStrList1 `StrList`
* @param pStrList2 `StrList`
*/
@@ -200,16 +215,14 @@ void strlist_remove(struct StrList *pStrList, size_t index) {
if (count == 0) {
return;
}
-
- for (size_t i = index; i < count; i++) {
- char *next = pStrList->data[i + 1];
- pStrList->data[i] = next;
- if (next == NULL) {
- break;
+ if (pStrList->data[index] != NULL) {
+ for (size_t i = index; i < count; i++) {
+ pStrList->data[i] = pStrList->data[i + 1];
+ }
+ if (pStrList->num_inuse) {
+ pStrList->num_inuse--;
}
}
-
- pStrList->num_inuse--;
}
/**
@@ -227,13 +240,10 @@ int strlist_cmp(struct StrList *a, struct StrList *b) {
return -2;
}
- if (a->num_alloc != b->num_alloc) {
+ if (a->num_alloc != b->num_alloc || a->num_inuse != b->num_inuse) {
return 1;
}
- if (a->num_inuse != b->num_inuse) {
- return 1;
- }
for (size_t i = 0; i < strlist_count(a); i++) {
if (strcmp(strlist_item(a, i), strlist_item(b, i)) != 0) {
@@ -321,6 +331,29 @@ void strlist_set(struct StrList **pStrList, size_t index, char *value) {
}
}
+const char *strlist_error_msgs[] = {
+ "success",
+ "index out of range",
+ "invalid value for type",
+ "unknown error",
+};
+int strlist_errno = 0;
+
+void strlist_set_error(int flag) {
+ strlist_errno = flag;
+}
+
+const char *strlist_get_error(int flag) {
+ if (flag < STRLIST_E_SUCCESS || flag > STRLIST_E_UNKNOWN) {
+ return strlist_error_msgs[STRLIST_E_UNKNOWN];
+ }
+ return strlist_error_msgs[flag];
+}
+
+void strlist_clear_error() {
+ strlist_errno = STRLIST_E_SUCCESS;
+}
+
/**
* Retrieve data from a `StrList`
* @param pStrList
@@ -351,7 +384,17 @@ char *strlist_item_as_str(struct StrList *pStrList, size_t index) {
* @return `char`
*/
char strlist_item_as_char(struct StrList *pStrList, size_t index) {
- return (char) strtol(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ char result;
+
+ strlist_clear_error();
+ result = (char) strtol(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -361,7 +404,17 @@ char strlist_item_as_char(struct StrList *pStrList, size_t index) {
* @return `unsigned char`
*/
unsigned char strlist_item_as_uchar(struct StrList *pStrList, size_t index) {
- return (unsigned char) strtol(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ unsigned char result;
+
+ strlist_clear_error();
+ result = (unsigned char) strtoul(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -371,7 +424,17 @@ unsigned char strlist_item_as_uchar(struct StrList *pStrList, size_t index) {
* @return `short`
*/
short strlist_item_as_short(struct StrList *pStrList, size_t index) {
- return (short)strtol(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ short result;
+
+ strlist_clear_error();
+ result = (short) strtol(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -381,7 +444,17 @@ short strlist_item_as_short(struct StrList *pStrList, size_t index) {
* @return `unsigned short`
*/
unsigned short strlist_item_as_ushort(struct StrList *pStrList, size_t index) {
- return (unsigned short)strtoul(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ unsigned short result;
+
+ strlist_clear_error();
+ result = (unsigned short) strtoul(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -391,7 +464,17 @@ unsigned short strlist_item_as_ushort(struct StrList *pStrList, size_t index) {
* @return `int`
*/
int strlist_item_as_int(struct StrList *pStrList, size_t index) {
- return (int)strtol(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ int result;
+
+ strlist_clear_error();
+ result = (int) strtol(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -401,7 +484,17 @@ int strlist_item_as_int(struct StrList *pStrList, size_t index) {
* @return `unsigned int`
*/
unsigned int strlist_item_as_uint(struct StrList *pStrList, size_t index) {
- return (unsigned int)strtoul(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ unsigned int result;
+
+ strlist_clear_error();
+ result = (unsigned int) strtoul(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -411,7 +504,17 @@ unsigned int strlist_item_as_uint(struct StrList *pStrList, size_t index) {
* @return `long`
*/
long strlist_item_as_long(struct StrList *pStrList, size_t index) {
- return strtol(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ long result;
+
+ strlist_clear_error();
+ result = (long) strtol(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -421,7 +524,17 @@ long strlist_item_as_long(struct StrList *pStrList, size_t index) {
* @return `unsigned long`
*/
unsigned long strlist_item_as_ulong(struct StrList *pStrList, size_t index) {
- return strtoul(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ unsigned long result;
+
+ strlist_clear_error();
+ result = (unsigned long) strtoul(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -431,7 +544,17 @@ unsigned long strlist_item_as_ulong(struct StrList *pStrList, size_t index) {
* @return `long long`
*/
long long strlist_item_as_long_long(struct StrList *pStrList, size_t index) {
- return strtoll(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ long long result;
+
+ strlist_clear_error();
+ result = (long long) strtoll(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -441,7 +564,17 @@ long long strlist_item_as_long_long(struct StrList *pStrList, size_t index) {
* @return `unsigned long long`
*/
unsigned long long strlist_item_as_ulong_long(struct StrList *pStrList, size_t index) {
- return strtoull(strlist_item(pStrList, index), NULL, 10);
+ char *error_p;
+ unsigned long long result;
+
+ strlist_clear_error();
+ result = (unsigned long long) strtol(strlist_item(pStrList, index), &error_p, 10);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -451,7 +584,17 @@ unsigned long long strlist_item_as_ulong_long(struct StrList *pStrList, size_t i
* @return `float`
*/
float strlist_item_as_float(struct StrList *pStrList, size_t index) {
- return (float)atof(strlist_item(pStrList, index));
+ char *error_p;
+ float result;
+
+ strlist_clear_error();
+ result = (float) strtof(strlist_item(pStrList, index), &error_p);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -461,7 +604,17 @@ float strlist_item_as_float(struct StrList *pStrList, size_t index) {
* @return `double`
*/
double strlist_item_as_double(struct StrList *pStrList, size_t index) {
- return atof(strlist_item(pStrList, index));
+ char *error_p;
+ double result;
+
+ strlist_clear_error();
+ result = (double) strtod(strlist_item(pStrList, index), &error_p);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
@@ -471,7 +624,17 @@ double strlist_item_as_double(struct StrList *pStrList, size_t index) {
* @return `long double`
*/
long double strlist_item_as_long_double(struct StrList *pStrList, size_t index) {
- return (long double)atof(strlist_item(pStrList, index));
+ char *error_p;
+ long double result;
+
+ strlist_clear_error();
+ result = (long double) strtold(strlist_item(pStrList, index), &error_p);
+ if (!result && error_p && *error_p != 0) {
+ strlist_set_error(STRLIST_E_INVALID_VALUE);
+ return 0;
+ }
+ error_p = NULL;
+ return result;
}
/**
diff --git a/src/system.c b/src/system.c
index ca2da97..d5e77ce 100644
--- a/src/system.c
+++ b/src/system.c
@@ -1,11 +1,8 @@
-//
-// Created by jhunk on 10/4/23.
-//
-
#include "system.h"
#include "omc.h"
int shell(struct Process *proc, char *args) {
+ struct Process selfproc;
FILE *fp_out = NULL;
FILE *fp_err = NULL;
pid_t pid;
@@ -13,6 +10,18 @@ int shell(struct Process *proc, char *args) {
status = 0;
errno = 0;
+ if (!proc) {
+ // provide our own proc structure
+ // albeit not accessible to the user
+ memset(&selfproc, 0, sizeof(selfproc));
+ proc = &selfproc;
+ }
+
+ if (!args) {
+ proc->returncode = -1;
+ return -1;
+ }
+
FILE *tp = NULL;
char *t_name;
t_name = xmkstemp(&tp, "w");
@@ -31,22 +40,20 @@ int shell(struct Process *proc, char *args) {
exit(1);
} else if (pid == 0) {
int retval;
- if (proc != NULL) {
- if (strlen(proc->f_stdout)) {
- fp_out = freopen(proc->f_stdout, "w+", stdout);
- }
+ if (strlen(proc->f_stdout)) {
+ fp_out = freopen(proc->f_stdout, "w+", stdout);
+ }
- if (strlen(proc->f_stderr)) {
- fp_err = freopen(proc->f_stderr, "w+", stderr);
- }
+ if (strlen(proc->f_stderr)) {
+ fp_err = freopen(proc->f_stderr, "w+", stderr);
+ }
- if (proc->redirect_stderr) {
- if (fp_err) {
- fclose(fp_err);
- fclose(stderr);
- }
- dup2(fileno(stdout), fileno(stderr));
+ if (proc->redirect_stderr) {
+ if (fp_err) {
+ fclose(fp_err);
+ fclose(stderr);
}
+ dup2(fileno(stdout), fileno(stderr));
}
retval = execl("/bin/bash", "bash", "-c", t_name, (char *) NULL);
@@ -54,7 +61,7 @@ int shell(struct Process *proc, char *args) {
remove(t_name);
}
- if (proc != NULL && strlen(proc->f_stdout)) {
+ if (strlen(proc->f_stdout)) {
if (fp_out != NULL) {
fflush(fp_out);
fclose(fp_out);
@@ -62,7 +69,7 @@ int shell(struct Process *proc, char *args) {
fflush(stdout);
fclose(stdout);
}
- if (proc != NULL && strlen(proc->f_stderr)) {
+ if (strlen(proc->f_stderr)) {
if (fp_err) {
fflush(fp_err);
fclose(fp_err);
@@ -89,10 +96,7 @@ int shell(struct Process *proc, char *args) {
remove(t_name);
}
- if (proc != NULL) {
- proc->returncode = status;
- }
-
+ proc->returncode = status;
guard_free(t_name);
return WEXITSTATUS(status);
}
@@ -102,7 +106,7 @@ int shell_safe(struct Process *proc, char *args) {
char buf[1024] = {0};
int result;
- char *invalid_ch = strpbrk(args, ";&|()");
+ char *invalid_ch = strpbrk(args, OMC_SHELL_SAFE_RESTRICT);
if (invalid_ch) {
args = NULL;
}
@@ -168,6 +172,6 @@ char *shell_output(const char *command, int *status) {
strcat(result, line);
memset(line, 0, sizeof(line));
}
- pclose(pp);
+ *status = pclose(pp);
return result;
} \ No newline at end of file
diff --git a/src/template.c b/src/template.c
index 6101338..0a57232 100644
--- a/src/template.c
+++ b/src/template.c
@@ -16,9 +16,14 @@ struct tpl_item {
};
struct tpl_item *tpl_pool[1024] = {0};
unsigned tpl_pool_used = 0;
+struct tplfunc_frame *tpl_pool_func[1024] = {0};
unsigned tpl_pool_func_used = 0;
-struct tplfunc_frame *tpl_pool_func[1024] = {0};
+extern void tpl_reset() {
+ tpl_free();
+ tpl_pool_used = 0;
+ tpl_pool_func_used = 0;
+}
void tpl_register_func(char *key, struct tplfunc_frame *frame) {
(void) key; // TODO: placeholder
@@ -86,6 +91,10 @@ void tpl_free() {
}
guard_free(item);
}
+ for (unsigned i = 0; i < tpl_pool_func_used; i++) {
+ struct tplfunc_frame *item = tpl_pool_func[i];
+ guard_free(item);
+ }
}
char *tpl_getval(char *key) {
@@ -167,7 +176,9 @@ char *tpl_render(char *str) {
size_t key_len = 0;
while (isalnum(pos[off]) || pos[off] != '}') {
if (isspace(pos[off]) || isblank(pos[off])) {
- break;
+ // skip whitespace in key
+ off++;
+ continue;
}
key[key_len] = pos[off];
key_len++;
@@ -205,7 +216,44 @@ char *tpl_render(char *str) {
char *env_val = getenv(key);
value = strdup(env_val ? env_val : "");
} else if (do_func) { // {{ func:NAME(a, ...) }}
- // TODO
+ char func_name_temp[OMC_NAME_MAX] = {0};
+ strcpy(func_name_temp, type_stop + 1);
+ char *param_begin = strchr(func_name_temp, '(');
+ if (!param_begin) {
+ fprintf(stderr, "offset %zu: function name must be followed by a '('\n", off);
+ guard_free(output);
+ return NULL;
+ }
+ *param_begin = 0;
+ param_begin++;
+ char *param_end = strrchr(param_begin, ')');
+ if (!param_end) {
+ fprintf(stderr, "offset %zu: function arguments must be closed with a ')'\n", off);
+ guard_free(output);
+ return NULL;
+ }
+ *param_end = 0;
+ char *k = func_name_temp;
+ char **params = split(param_begin, ",", 0);
+ int params_count;
+ for (params_count = 0; params[params_count] != NULL; params_count++);
+
+ struct tplfunc_frame *frame = tpl_getfunc(k);
+ if (params_count > frame->argc) {
+ fprintf(stderr, "offset %zu: Too many arguments for function: %s()\n", off, frame->key);
+ value = strdup("");
+ } else {
+ for (size_t p = 0; p < sizeof(frame->argv) / sizeof(*frame->argv) && params[p] != NULL; p++) {
+ lstrip(params[p]);
+ strip(params[p]);
+ frame->argv[p].t_char_ptr = params[p];
+ }
+ char func_result[100];
+ char *fres = func_result;
+ frame->func(frame, fres);
+ value = strdup(fres);
+ }
+ GENERIC_ARRAY_FREE(params);
} else {
// Read replacement value
value = strdup(tpl_getval(key) ? tpl_getval(key) : "");
diff --git a/src/utils.c b/src/utils.c
index 7cc3e6e..86622ad 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -6,9 +6,15 @@ const ssize_t dirstack_max = sizeof(dirstack) / sizeof(dirstack[0]);
ssize_t dirstack_len = 0;
int pushd(const char *path) {
+ if (access(path, F_OK)) {
+ // the requested path doesn't exist.
+ return -1;
+ }
if (dirstack_len + 1 > dirstack_max) {
+ // the stack is full
return -1;
}
+
dirstack[dirstack_len] = realpath(".", NULL);
dirstack_len++;
return chdir(path);
@@ -151,7 +157,7 @@ char *path_dirname(char *path) {
if (!pos) {
return ".";
}
- *path = '\0';
+ *pos = '\0';
return path;
}
@@ -701,30 +707,26 @@ char *collapse_whitespace(char **s) {
* @param maxlen maximum length of dest string
* @return 0 on success, -1 on error
*/
-int redact_sensitive(const char **to_redact, char *src, char *dest, size_t maxlen) {
- char **parts = split(src, " ", 0);
- if (!parts) {
- fprintf(stderr, "Unable to split source string\n");
+int redact_sensitive(const char **to_redact, size_t to_redact_size, char *src, char *dest, size_t maxlen) {
+ const char *redacted = "***REDACTED***";
+
+ char *tmp = calloc(strlen(redacted) + strlen(src) + 1, sizeof(*tmp));
+ if (!tmp) {
return -1;
}
+ strcpy(tmp, src);
- for (size_t i = 0; to_redact[i] != NULL; i++) {
- for (size_t p = 0; parts[p] != NULL; p++) {
- if (strstr(parts[p], to_redact[i])) {
- replace_text(parts[p], to_redact[i], "***REDACTED***", REPLACE_TRUNCATE_AFTER_MATCH);
- }
+ for (size_t i = 0; i < to_redact_size; i++) {
+ if (to_redact[i] && strstr(tmp, to_redact[i])) {
+ replace_text(tmp, to_redact[i], redacted, 0);
+ break;
}
}
- char *dest_tmp = join(parts, " ");
- if (!dest_tmp) {
- fprintf(stderr, "Unable to join message array\n");
- return -1;
- }
- strncpy(dest, dest_tmp, maxlen);
+ memset(dest, 0, maxlen);
+ strncpy(dest, tmp, maxlen - 1);
+ guard_free(tmp);
- GENERIC_ARRAY_FREE(parts);
- guard_free(dest_tmp);
return 0;
}
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