aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@users.noreply.github.com>2026-05-06 14:29:21 -0400
committerGitHub <noreply@github.com>2026-05-06 14:29:21 -0400
commitb494ddd036f9b17fcfabd42decd325bbe8be914e (patch)
tree4b722c242e454332d3eef596b68f2fad9d935fe1
parente412ae6ff1aa793799ccdbe893484273e71e909a (diff)
parent17bd286713df8f76f7b5f3878e501a81fa4c04ee (diff)
downloadstasis-b494ddd036f9b17fcfabd42decd325bbe8be914e.tar.gz
Merge pull request #140 from jhunkeler/version-compare
Add version comparison code
-rw-r--r--src/cli/stasis/stasis_main.c32
-rw-r--r--src/lib/core/CMakeLists.txt1
-rw-r--r--src/lib/core/include/strlist.h6
-rw-r--r--src/lib/core/include/version_compare.h20
-rw-r--r--src/lib/core/strlist.c59
-rw-r--r--src/lib/core/utils.c39
-rw-r--r--src/lib/core/version_compare.c185
-rw-r--r--src/lib/delivery/delivery_install.c136
-rw-r--r--src/lib/delivery/include/delivery.h11
-rw-r--r--tests/include/testing.h27
-rw-r--r--tests/test_version_compare.c172
11 files changed, 637 insertions, 51 deletions
diff --git a/src/cli/stasis/stasis_main.c b/src/cli/stasis/stasis_main.c
index 78aae0c..95cb7c5 100644
--- a/src/cli/stasis/stasis_main.c
+++ b/src/cli/stasis/stasis_main.c
@@ -323,6 +323,37 @@ static void install_packaging_tools() {
}
}
+static void force_conda_package_reinstallation_on_mismatch(struct Delivery *ctx, const char *env_name) {
+ const size_t conda_package_count = strlist_count(ctx->conda.conda_packages);
+ if (conda_package_count) {
+ msg(STASIS_MSG_L1, "Enforcing Conda package versions: %s\n", env_name);
+ for (size_t i = 0; i < conda_package_count; i++) {
+ const char *item = strlist_item(ctx->conda.conda_packages, i);
+ if (!item) {
+ msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "NULL record in conda package list\n");
+ exit(1);
+ }
+ char *pkg_name = strdup(item);
+ if (!pkg_name) {
+ msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "unable to allocate memory for package name\n");
+ exit(1);
+ }
+ const char *spec = find_version_spec(pkg_name);
+ if (spec) {
+ pkg_name[spec - pkg_name] = '\0';
+ }
+
+ msg(STASIS_MSG_L2, "%s\n", pkg_name);
+ if (delivery_conda_enforce_package_version(ctx, env_name, pkg_name)) {
+ msg(STASIS_MSG_L3 | STASIS_MSG_ERROR, "Failed to determine conda package version: %s\n", pkg_name);
+ guard_free(pkg_name);
+ exit(1);
+ }
+ guard_free(pkg_name);
+ }
+ }
+}
+
static void configure_package_overlay(struct Delivery *ctx, const char *env_name) {
if (!isempty(ctx->meta.based_on)) {
msg(STASIS_MSG_L1, "Generating package overlay from environment: %s\n", env_name);
@@ -398,6 +429,7 @@ static void release_install_conda_packages(struct Delivery *ctx, char *env_name)
if (delivery_install_packages(ctx, ctx->storage.conda_install_prefix, env_name, INSTALL_PKG_CONDA, (struct StrList *[]) {ctx->conda.conda_packages, NULL})) {
exit(1);
}
+ force_conda_package_reinstallation_on_mismatch(ctx, env_name);
}
if (strlist_count(ctx->conda.conda_packages_defer)) {
msg(STASIS_MSG_L3, "Installing deferred conda packages\n");
diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt
index 0fb273c..462d7d8 100644
--- a/src/lib/core/CMakeLists.txt
+++ b/src/lib/core/CMakeLists.txt
@@ -24,6 +24,7 @@ add_library(stasis_core STATIC
envctl.c
multiprocessing.c
semaphore.c
+ version_compare.c
)
target_include_directories(stasis_core PRIVATE
${core_INCLUDE}
diff --git a/src/lib/core/include/strlist.h b/src/lib/core/include/strlist.h
index b2d7da7..f44025c 100644
--- a/src/lib/core/include/strlist.h
+++ b/src/lib/core/include/strlist.h
@@ -45,8 +45,10 @@ int strlist_append_file(struct StrList *pStrList, char *path, ReaderFn *readerFn
void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2);
void strlist_append(struct StrList **pStrList, char *str);
void strlist_append_array(struct StrList *pStrList, char **arr);
-void strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim);
-void strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim);
+
+int strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim);
+
+int strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim);
int strlist_appendf(struct StrList **pStrList, const char *fmt, ...);
struct StrList *strlist_copy(struct StrList *pStrList);
int strlist_cmp(struct StrList *a, struct StrList *b);
diff --git a/src/lib/core/include/version_compare.h b/src/lib/core/include/version_compare.h
new file mode 100644
index 0000000..857de47
--- /dev/null
+++ b/src/lib/core/include/version_compare.h
@@ -0,0 +1,20 @@
+#ifndef STASIS_VERSION_COMPARE_H
+#define STASIS_VERSION_COMPARE_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include "str.h"
+
+#define GT 1 << 1
+#define LT 1 << 2
+#define EQ 1 << 3
+#define NOT 1 << 4
+#define EPOCH_MOD 10000
+
+int version_sum(const char *str);
+int version_parse_operator(const char *str);
+int version_compare(int flags, const char *aa, const char *bb);
+
+#endif //STASIS_VERSION_COMPARE_H \ No newline at end of file
diff --git a/src/lib/core/strlist.c b/src/lib/core/strlist.c
index 42d5b85..0d25a66 100644
--- a/src/lib/core/strlist.c
+++ b/src/lib/core/strlist.c
@@ -218,22 +218,30 @@ void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2
* @param str
* @param delim
*/
- void strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim) {
- if (!str || !delim) {
- return;
- }
+int strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim) {
+ if (!str || !delim) {
+ return -1;
+ }
- char *tmp = strdup(str);
- char **token = split(tmp, delim, 0);
- if (token) {
- for (size_t i = 0; token[i] != NULL; i++) {
- lstrip(token[i]);
- strlist_append(&pStrList, token[i]);
- }
- guard_array_free(token);
- }
+ char *tmp = strdup(str);
+ if (!tmp) {
+ return -1;
+ }
+
+ char **token = split(tmp, delim, 0);
+ if (!token) {
+ guard_free(tmp);
+ return -1;
+ }
+ for (size_t i = 0; token[i] != NULL; i++) {
+ lstrip(token[i]);
+ strlist_append(&pStrList, token[i]);
+ }
+ guard_array_free(token);
guard_free(tmp);
- }
+
+ return 0;
+}
/**
* Append the contents of a newline delimited string without
@@ -242,20 +250,29 @@ void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2
* @param str
* @param delim
*/
-void strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim) {
+int strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim) {
if (!str || !delim) {
- return;
+ return -1;
}
char *tmp = strdup(str);
+ if (!tmp) {
+ return -1;
+ }
+
char **token = split(tmp, delim, 0);
- if (token) {
- for (size_t i = 0; token[i] != NULL; i++) {
- strlist_append(&pStrList, token[i]);
- }
- guard_array_free(token);
+ if (!token) {
+ guard_free(tmp);
+ return -1;
}
+
+ for (size_t i = 0; token[i] != NULL; i++) {
+ strlist_append(&pStrList, token[i]);
+ }
+
+ guard_array_free(token);
guard_free(tmp);
+ return 0;
}
/**
diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c
index 90dac1d..e6a8315 100644
--- a/src/lib/core/utils.c
+++ b/src/lib/core/utils.c
@@ -742,29 +742,32 @@ int fix_tox_conf(const char *filename, char **result, size_t maxlen) {
return 0;
}
-static size_t count_blanks(char *s) {
- // return the number of leading blanks (tab/space) in a string
- size_t blank = 0;
- for (size_t i = 0; i < strlen(s); i++) {
- if (isblank(s[i])) {
- blank++;
+/**
+ * Collapse all whitespace in a string (to single spaces)
+ * @param s address of string to modify
+ * @return
+ */
+char *collapse_whitespace(char **s) {
+ char *dest = NULL;
+ char *src = NULL;
+ int in_ws = 1;
+
+ for (src = dest = *s; *src != '\0'; ++src) {
+ if (isspace(*src)) {
+ if (!in_ws) {
+ *dest++ = ' ';
+ in_ws = 1;
+ }
} else {
- break;
+ *dest++ = *src;
+ in_ws = 0;
}
}
- return blank;
-}
-char *collapse_whitespace(char **s) {
- char *x = (*s);
- size_t len = strlen(x);
- for (size_t i = 0; i < len; i++) {
- size_t blank = count_blanks(&x[i]);
- if (blank > 1) {
- memmove(&x[i], &x[i] + blank, strlen(&x[i]));
- }
+ if (dest > *s && *(dest - 1) == ' ') {
+ --dest;
}
-
+ *dest = '\0';
return *s;
}
diff --git a/src/lib/core/version_compare.c b/src/lib/core/version_compare.c
new file mode 100644
index 0000000..4939c8f
--- /dev/null
+++ b/src/lib/core/version_compare.c
@@ -0,0 +1,185 @@
+#include "version_compare.h"
+
+const struct {
+ const char *key;
+ int value;
+} WEIGHT[] = {
+ {.key = "post", 1000},
+ {.key = "rc", -1000},
+ {.key = "dev", -2000},
+};
+
+/**
+ * Sum each part of a '.'-delimited version string
+ * @param str version string
+ * @return sum of each part
+ * @return -1 on error
+ */
+int version_sum(const char *str) {
+ char *end;
+
+ if (!str || isempty((char *) str)) {
+ return -1;
+ }
+
+ int result = 0;
+ int epoch = 0;
+ char *s = strdup(str);
+ if (!s) {
+ return -1;
+ }
+ char *ptr = s;
+ end = ptr;
+
+ // Parsing stops at the first non-alpha, non-'.' character
+ // Digits are processed until the first invalid character
+ // I'm torn whether this should be considered an error
+ int i = 0;
+ while (end != NULL) {
+ int tmp_result = 0;
+
+ tmp_result = (int) strtoul(ptr, &end, 10);
+
+ // Circumvent a bug which allows a smaller version to be greater
+ // than a larger version
+ // Bug:
+ // 1.0.3 == 1 + 0 + 3 = 4
+ // 2.0.0 == 2 + 0 + 0 = 2
+ // Correction:
+ // ((1 * EPOCH_MOD) + 1).0.3 = 104
+ // ((2 * EPOCH_MOD) + 2).0.0 = 202
+ if (!i && tmp_result && *end != ':') {
+ result += tmp_result * EPOCH_MOD;
+ i++;
+ }
+
+ ptr = end;
+ if (*ptr == '.' || *ptr == '-') {
+ ptr++;
+ }
+ else if (!epoch && *ptr == ':') {
+ epoch = 1;
+ result += EPOCH_MOD;
+ ptr++;
+ }
+ else if (isalpha(*ptr)) {
+ int adjusted = 0;
+ for (size_t w = 0; w < sizeof(WEIGHT) / sizeof(WEIGHT[0]); w++) {
+ const int has_suffix = strncasecmp(ptr, WEIGHT[w].key, strlen(WEIGHT[w].key)) == 0;
+ if (has_suffix) {
+ // skip the suffix
+ ptr += strlen(WEIGHT[w].key);
+ // adjust result based on suffix weight
+ result += WEIGHT[w].value;
+ adjusted = 1;
+ break;
+ }
+ }
+
+ if (!adjusted) {
+ result += *ptr - ('a' - 1);
+ ptr++;
+ }
+ }
+ else {
+ end = NULL;
+ }
+
+ if (tmp_result) {
+ result += tmp_result;
+ }
+ }
+
+ free(s);
+ return result;
+}
+
+/**
+ * Convert version operator(s) to flags
+ * @param str input string
+ * @return operator flags
+ */
+int version_parse_operator(const char *str) {
+ const char *valid = "><=!";
+
+ const char *pos = str;
+ int result = 0;
+
+ if (isempty((char *) str)) {
+ return -1;
+ }
+ while ((pos = strpbrk(pos, valid)) != NULL) {
+ switch (*pos) {
+ case '>':
+ result |= GT;
+ break;
+ case '<':
+ result |= LT;
+ break;
+ case '=':
+ result |= EQ;
+ break;
+ case '!':
+ result |= NOT;
+ break;
+ default:
+ return -1;
+ }
+ pos++;
+ }
+
+ return result;
+}
+
+static int version_has_epoch(const char *str) {
+ const char *result = strchr(str, ':');
+ return result ? 1 : 0;
+}
+
+/**
+ * Compare version strings based on flag(s)
+ * @param flags verison operators
+ * @param aa version1
+ * @param bb version2
+ * @return 1 flag operation is true
+ * @return 0 flag operation is false
+ */
+int version_compare(const int flags, const char *aa, const char *bb) {
+ if (!flags || flags < 0) {
+ return -1;
+ }
+
+ int result_a = version_sum(aa);
+ if (result_a < 0) {
+ return -1;
+ }
+
+ int result_b = version_sum(bb);
+ if (result_b < 0) {
+ return -1;
+ }
+
+ if (version_has_epoch(aa) && !version_has_epoch(bb)) {
+ result_a -= EPOCH_MOD;
+ }
+ if (!version_has_epoch(aa) && version_has_epoch(bb)) {
+ result_b -= EPOCH_MOD;
+ }
+
+ int result = 0;
+ if (flags & GT && flags & EQ) {
+ result |= result_a >= result_b;
+ } else if (flags & LT && flags & EQ) {
+ result |= result_a <= result_b;
+ } else if (flags & NOT && flags & EQ) {
+ result |= result_a != result_b;
+ } else if (flags & GT) {
+ result |= result_a > result_b;
+ } else if (flags & LT) {
+ result |= result_a < result_b;
+ } else if (flags & EQ) {
+ result |= result_a == result_b;
+ }
+
+ return result;
+} \ No newline at end of file
diff --git a/src/lib/delivery/delivery_install.c b/src/lib/delivery/delivery_install.c
index 3d54eaa..6ad9407 100644
--- a/src/lib/delivery/delivery_install.c
+++ b/src/lib/delivery/delivery_install.c
@@ -134,6 +134,142 @@ int delivery_overlay_packages_from_env(struct Delivery *ctx, const char *env_nam
return 0;
}
+int delivery_conda_enforce_package_version(struct Delivery *ctx, const char *env_name, const char *name) {
+ char *spec_installed = NULL;
+ char *spec_request = NULL;
+ int status = 0;
+
+ if (isempty((char *) env_name)) {
+ SYSERROR("%s", "environment name cannot be NULL or empty");
+ return -1;
+ }
+ if (isempty((char *) name)) {
+ SYSERROR("%s", "name cannot be NULL or empty");
+ return -1;
+ }
+
+ int proc_status = 0;
+ char cmd[PATH_MAX] = {0};
+ snprintf(cmd, PATH_MAX, "conda list --name %s", env_name);
+
+ char *output = shell_output(cmd, &proc_status);
+ if (!output || proc_status) {
+ SYSERROR("unable to retreive list of installed packages (exit: %d)", proc_status);
+ guard_free(output);
+ return -1;
+ }
+
+ struct StrList *lines = strlist_init();
+ if (!lines) {
+ SYSERROR("%s", "unable to allocate memory for installed package list");
+ guard_free(output);
+ status = -1;
+ goto cleanup;
+ }
+
+ if (strlist_append_tokenize(lines, output, LINE_SEP)) {
+ SYSERROR("%s", "unable to tokenize installed package list");
+ guard_free(output);
+ strlist_free(&lines);
+ status = -1;
+ goto cleanup;
+ }
+
+ for (size_t i = 0; i < strlist_count(lines); i++) {
+ char *line = strlist_item(lines, i);
+ if (!line) {
+ SYSERROR("%s", "line is NULL");
+ status = -1;
+ goto cleanup;
+ }
+ if (startswith(line, "#") || isempty(line)) {
+ continue;
+ }
+ collapse_whitespace(&line);
+ strip(line);
+
+ struct StrList *tokens = strlist_init();
+ if (!tokens) {
+ SYSERROR("%s", "unable to allocate memory for tokenized installed package list");
+ status = -1;
+ goto cleanup;
+ }
+
+ if (strlist_append_tokenize(tokens, line, " ")) {
+ SYSERROR("%s", "unable to tokenize installed package list");
+ status = -1;
+ goto cleanup;
+ }
+
+ const char *installed_version = strlist_item(tokens, 1);
+ if (!installed_version) {
+ SYSERROR("%s", "not enough data in line (name and version not found)");
+ guard_strlist_free(&tokens);
+ status = -1;
+ goto cleanup;
+ }
+
+ if (strstr(line, name)) {
+ spec_installed = strdup(installed_version);
+ if (!spec_installed) {
+ SYSERROR("%s", "unable to allocated memory for installed package version");
+ guard_strlist_free(&tokens);
+ status = -1;
+ goto cleanup;
+ }
+ guard_strlist_free(&tokens);
+ break;
+ }
+
+ guard_strlist_free(&tokens);
+ }
+
+ for (size_t i = 0; i < strlist_count(ctx->conda.conda_packages); i++) {
+ const char *item = strlist_item(ctx->conda.conda_packages, i);
+ if (!item) {
+ SYSERROR("conda_packages list record %zu is NULL", i);
+ status = -1;
+ goto cleanup;
+ }
+ if (strstr(item, name)) {
+ char *spec_tmp = find_version_spec((char *) item);
+ while (!isalnum(*spec_tmp)) {
+ spec_tmp++;
+ }
+
+ spec_request = strdup(spec_tmp);
+ if (!spec_request) {
+ SYSERROR("%s", "unable to allocate memory for conda package spec request");
+ status = -1;
+ goto cleanup;
+ }
+ break;
+ }
+ }
+
+ const int stop = version_compare(NOT | EQ, spec_request, spec_installed);
+ if (stop < 0) {
+ SYSERROR("version comparison failed (spec_request: %s, spec_installed: %s)", spec_request, spec_installed);
+ status = -1;
+ goto cleanup;
+ }
+ if (stop == 0) {
+ goto cleanup;
+ }
+
+ snprintf(cmd, PATH_MAX, "remove --name %s %s", env_name, name);
+ conda_exec(cmd);
+ snprintf(cmd, PATH_MAX, "install --name %s %s=%s", env_name, name, spec_request);
+ conda_exec(cmd);
+
+ cleanup:
+ guard_free(spec_request);
+ guard_free(spec_installed);
+ strlist_free(&lines);
+ guard_free(output);
+ return status;
+}
+
static int fn_nop(const char *command) {
(void) command;
return 1;
diff --git a/src/lib/delivery/include/delivery.h b/src/lib/delivery/include/delivery.h
index e524f4d..c091182 100644
--- a/src/lib/delivery/include/delivery.h
+++ b/src/lib/delivery/include/delivery.h
@@ -21,6 +21,7 @@
#include "wheel.h"
#include "wheelinfo.h"
#include "environment.h"
+#include "version_compare.h"
#define DELIVERY_PLATFORM_MAX 4
#define DELIVERY_PLATFORM_MAXLEN 65
@@ -451,6 +452,16 @@ int delivery_exists(struct Delivery *ctx);
int delivery_overlay_packages_from_env(struct Delivery *ctx, const char *env_name);
/**
+ * Conda does not handle version suffixes well, if at all. For example, if pkg-1.2.3rc1 is installed Conda will
+ * silently ignore a request to install pkg-1.2.3. This function serves as a workaround by comparing the version
+ * on-disk, and the requested version from the package list, and if the versions are not equal the on-disk package
+ * is replaced by the one in the package list.
+ *
+ * When a package is present in the list without a pinned version it will be reinstalled with whatever is available
+ */
+int delivery_conda_enforce_package_version(struct Delivery *ctx, const char *env_name, const char *name);
+
+/**
* Retrieve remote deliveries associated with the current version series
* @param ctx Delivery context
* @return -1 on error
diff --git a/tests/include/testing.h b/tests/include/testing.h
index 80d5aa2..e446908 100644
--- a/tests/include/testing.h
+++ b/tests/include/testing.h
@@ -57,32 +57,39 @@ inline void stasis_testing_record_result_summary() {
size_t failed = 0;
size_t skipped = 0;
size_t passed = 0;
- int do_message;
+ int do_message = 0;
+ int do_reason = 0;
static char status_msg[255] = {0};
for (size_t i = 0; i < stasis_test_results_i; i++) {
if (stasis_test_results[i].status && stasis_test_results[i].skip) {
strcpy(status_msg, "SKIP");
do_message = 1;
+ do_reason = 1;
skipped++;
} else if (!stasis_test_results[i].status) {
strcpy(status_msg, "FAIL");
do_message = 1;
+ do_reason = 1;
failed++;
} else {
+#ifdef STASIS_TEST_VERBOSE
+ do_message = 1;
+#endif
strcpy(status_msg, "PASS");
- do_message = 0;
passed++;
}
- fprintf(stdout, "[%s] %s:%d :: %s() => %s",
- status_msg,
- stasis_test_results[i].filename,
- stasis_test_results[i].lineno,
- stasis_test_results[i].funcname,
- stasis_test_results[i].msg_assertion);
if (do_message) {
- fprintf(stdout, "\n \\_ %s", stasis_test_results[i].msg_reason);
+ fprintf(stdout, "[%s] %s:%d :: %s() => %s",
+ status_msg,
+ stasis_test_results[i].filename,
+ stasis_test_results[i].lineno,
+ stasis_test_results[i].funcname,
+ stasis_test_results[i].msg_assertion);
+ if (do_reason) {
+ fprintf(stdout, "\n \\_ %s", stasis_test_results[i].msg_reason);
+ }
+ fprintf(stdout, "\n");
}
- fprintf(stdout, "\n");
}
fprintf(stdout, "\n[UNIT] %zu tests passed, %zu tests failed, %zu tests skipped out of %zu\n", passed, failed, skipped, stasis_test_results_i);
}
diff --git a/tests/test_version_compare.c b/tests/test_version_compare.c
new file mode 100644
index 0000000..2a3458f
--- /dev/null
+++ b/tests/test_version_compare.c
@@ -0,0 +1,172 @@
+#include "testing.h"
+#include "version_compare.h"
+
+struct TestCase_version_compare {
+ char *a, *op, *b;
+ int expected;
+};
+
+struct TestCase_version_compare test_cases_version_compare[] = {
+ {"0", "=", "0", 1},
+ {"0", "<", "1",1},
+ {"0", "<=", "1",1},
+ {"0", ">", "1",0},
+ {"0", ">=", "1",0},
+ {"0", "!=", "1",1},
+
+ {"1a", "=", "1b", 0},
+ {"1a", "<", "1b", 1},
+ {"1a", "<=", "1b", 1},
+ {"1a", ">", "1b", 0},
+ {"1a", ">=", "1b", 0},
+ {"1a", "!=", "1b", 1},
+
+ {"1.0", "=", "1.0.0", 1},
+ {"1.0", "<", "1.0.0", 0},
+ {"1.0", "<=", "1.0.0", 1},
+ {"1.0", ">", "1.0.0", 0},
+ {"1.0", ">=", "1.0.0", 1},
+ {"1.0", "!=", "1.0.0", 0},
+
+ {"1.0rc1", "=", "1.0.0", 0},
+ {"1.0rc1", "<", "1.0.0", 1},
+ {"1.0rc1", "<=", "1.0.0", 1},
+ {"1.0rc1", ">", "1.0.0", 0},
+ {"1.0rc1", ">=", "1.0.0", 0},
+ {"1.0rc1", "!=", "1.0.0", 1},
+
+ {"1.0rc1", "=", "1.0.0rc1", 1},
+ {"1.0rc1", "<", "1.0.0rc1", 0},
+ {"1.0rc1", "<=", "1.0.0rc1", 1},
+ {"1.0rc1", ">", "1.0.0rc1", 0},
+ {"1.0rc1", ">=", "1.0.0rc1", 1},
+ {"1.0rc1", "!=", "1.0.0rc1", 0},
+
+ {"1.0rc1", "=", "1.0.0dev1", 0},
+ {"1.0rc1", "<", "1.0.0dev1", 0},
+ {"1.0rc1", "<=", "1.0.0dev1", 0},
+ {"1.0rc1", ">", "1.0.0dev1", 1},
+ {"1.0rc1", ">=", "1.0.0dev1", 1},
+ {"1.0rc1", "!=", "1.0.0dev1", 1},
+
+ {"1.0post1", "=", "1.0.0dev1", 0},
+ {"1.0post1", "<", "1.0.0dev1", 0},
+ {"1.0post1", "<=", "1.0.0dev1", 0},
+ {"1.0post1", ">", "1.0.0dev1", 1},
+ {"1.0post1", ">=", "1.0.0dev1", 1},
+ {"1.0post1", "!=", "1.0.0dev1", 1},
+
+ {"1.0post1", "=", "1.0.0", 0},
+ {"1.0post1", "<", "1.0.0", 0},
+ {"1.0post1", "<=", "1.0.0", 0},
+ {"1.0post1", ">", "1.0.0", 1},
+ {"1.0post1", ">=", "1.0.0", 1},
+ {"1.0post1", "!=", "1.0.0", 1},
+
+ {"1.0dev1", "=", "1.0.0dev1", 1},
+ {"1.0dev1", "<", "1.0.0dev1", 0},
+ {"1.0dev1", "<=", "1.0.0dev1", 1},
+ {"1.0dev1", ">", "1.0.0dev1", 0},
+ {"1.0dev1", ">=", "1.0.0dev1", 1},
+ {"1.0dev1", "!=", "1.0.0dev1", 0},
+
+ {"1.0a", "=", "1.0.0", 0},
+ {"1.0a", "<", "1.0.0", 0},
+ {"1.0a", "<=", "1.0.0", 0},
+ {"1.0a", ">", "1.0.0", 1},
+ {"1.0a", ">=", "1.0.0", 1},
+ {"1.0a", "!=", "1.0.0", 1},
+
+ {"1.0.3", "=", "2.0.0", 0},
+ {"1.0.3", "<", "2.0.0", 1},
+ {"1.0.3", "<=", "2.0.0", 1},
+ {"1.0.3", ">", "2.0.0", 0},
+ {"1.0.3", ">=", "2.0.0", 0},
+ {"1.0.3", "!=", "2.0.0", 1},
+
+ {"2022.1", "=", "2022.4", 0},
+ {"2022.1", "<", "2022.4", 1},
+ {"2022.1", "<=", "2022.4", 1},
+ {"2022.1", ">", "2022.4", 0},
+ {"2022.1", ">=", "2022.4", 0},
+ {"2022.1", "!=", "2022.4", 1},
+
+ {"1:2022.1", "=", "2022.4", 0},
+ {"1:2022.1", "<", "2022.4", 1},
+ {"1:2022.1", "<=", "2022.4", 1},
+ {"1:2022.1", ">", "2022.4", 0},
+ {"1:2022.1", ">=", "2022.4", 0},
+ {"1:2022.1", "!=", "2022.4", 1},
+
+ {"1:2022.1", "=", "2:2022.4", 0},
+ {"1:2022.1", "<", "2:2022.4", 1},
+ {"1:2022.1", "<=", "2:2022.4", 1},
+ {"1:2022.1", ">", "2:2022.4", 0},
+ {"1:2022.1", ">=", "2:2022.4", 0},
+ {"1:2022.1", "!=", "2:2022.4", 1},
+
+ {"2:2022.4", "=", "1:2022.1", 0},
+ {"2:2022.4", "<", "1:2022.1", 0},
+ {"2:2022.4", "<=", "1:2022.1", 0},
+ {"2:2022.4", ">", "1:2022.1", 1},
+ {"2:2022.4", ">=", "1:2022.1", 1},
+ {"2:2022.4", "!=", "1:2022.1", 1},
+
+ {"2022.1", "=", "2:2022.1", 0},
+ {"2022.1", "<", "2:2022.1", 1},
+ {"2022.1", "<=", "2:2022.1", 1},
+ {"2022.1", ">", "2:2022.1", 0},
+ {"2022.1", ">=", "2:2022.1", 0},
+ {"2022.1", "!=", "2:2022.1", 1},
+
+ {"2022.4", "=", "2022.1", 0},
+ {"2022.4", "<", "2022.1", 0},
+ {"2022.4", "<=", "2022.1", 0},
+ {"2022.4", ">", "2022.1", 1},
+ {"2022.4", ">=", "2022.1", 1},
+ {"2022.4", "!=", "2022.1", 1},
+
+ // Error cases
+ {NULL, "", "", -1},
+ {"", NULL, "", -1},
+ {"", "", NULL, -1},
+ {NULL, NULL, NULL, -1},
+ {"", "=", "", -1},
+ {" ", "=", " ", -1},
+ {"a", "", "a", -1},
+ {"a", "", "b", -1},
+ {"a", "@", "b", -1},
+};
+
+void run_cases_version_compare(void) {
+ const size_t size = sizeof(test_cases_version_compare) / sizeof(test_cases_version_compare[0]);
+ for (size_t i = 0; i < size; i++) {
+ int result = 0;
+ const struct TestCase_version_compare *test = &test_cases_version_compare[i];
+ const int op = version_parse_operator(test->op);
+ result = version_compare(op, test->a, test->b);
+ STASIS_ASSERT(test->expected == result, "unexpected result");
+
+ fprintf(stderr, "'%s' '%s' '%s' is %s (%d)",
+ test->a ? test->a : "NULL",
+ test->op ? test->op : "NULL",
+ test->b ? test->b : "NULL",
+ result == test->expected ? "EXPECTED" : "UNEXPECTED",
+ result);
+ if (test->expected != result) {
+ fprintf(stderr, " [FAILED: got %d, expected %d]\n", result, test->expected);
+ } else {
+ puts("");
+ }
+ }
+}
+
+int main(void) {
+ STASIS_TEST_BEGIN_MAIN();
+ STASIS_TEST_FUNC *tests[] = {
+ run_cases_version_compare,
+ };
+ STASIS_TEST_RUN(tests);
+
+ STASIS_TEST_END_MAIN();
+}