diff options
| -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 | 
