diff options
author | Joseph Hunkeler <jhunkeler@users.noreply.github.com> | 2024-06-20 15:10:56 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-20 15:10:56 -0400 |
commit | 931ee28eb9c5b5e3c2b0d3008f5f65d810dc9b0c (patch) | |
tree | 5dbcccffd509fa71a99c351ed4628ed0841e1e46 | |
parent | 11aa1d44d95da221073e512fbec3bbccc0f1a46b (diff) | |
download | ohmycal-931ee28eb9c5b5e3c2b0d3008f5f65d810dc9b0c.tar.gz |
* 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.yml | 12 | ||||
-rw-r--r-- | CMakeLists.txt | 10 | ||||
-rw-r--r-- | include/ini.h | 2 | ||||
-rw-r--r-- | include/str.h | 31 | ||||
-rw-r--r-- | include/strlist.h | 8 | ||||
-rw-r--r-- | include/system.h | 2 | ||||
-rw-r--r-- | include/template.h | 2 | ||||
-rw-r--r-- | include/utils.h | 2 | ||||
-rw-r--r-- | src/artifactory.c | 15 | ||||
-rw-r--r-- | src/docker.c | 4 | ||||
-rw-r--r-- | src/ini.c | 2 | ||||
-rw-r--r-- | src/str.c | 154 | ||||
-rw-r--r-- | src/strlist.c | 239 | ||||
-rw-r--r-- | src/system.c | 54 | ||||
-rw-r--r-- | src/template.c | 54 | ||||
-rw-r--r-- | src/utils.c | 38 | ||||
-rw-r--r-- | tests/CMakeLists.txt | 32 | ||||
-rw-r--r-- | tests/test_docker.c | 73 | ||||
-rw-r--r-- | tests/test_ini.c | 108 | ||||
-rw-r--r-- | tests/test_relocation.c | 62 | ||||
-rw-r--r-- | tests/test_str.c | 377 | ||||
-rw-r--r-- | tests/test_strlist.c | 617 | ||||
-rw-r--r-- | tests/test_system.c | 145 | ||||
-rw-r--r-- | tests/test_template.c | 100 | ||||
-rw-r--r-- | tests/test_utils.c | 479 | ||||
-rw-r--r-- | tests/testing.h | 174 |
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]; @@ -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) { @@ -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 |