aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@users.noreply.github.com>2024-07-15 10:07:25 -0400
committerGitHub <noreply@github.com>2024-07-15 10:07:25 -0400
commit07dc44efdc5c2fbc2b34c969e623d3b0bc0df15a (patch)
tree1f41c27e50baeee149b59b8c3d37a9c72cbd0ded
parent70cd78cdef69237ba3c511b9e091715ec6d093e5 (diff)
downloadstasis-07dc44efdc5c2fbc2b34c969e623d3b0bc0df15a.tar.gz
Unit tests (#12)
* Change return value of conda_setup_headless() from void to int * Replace exit() with return; * Return early if unpacking the micromamba binary fails * Exit program when pointer to INIFILE is NULL. * Validation function cannot otherwise proceed * The way the logic is set up I've decided to duplicate the installation code for now until I find time to revise it * The only meaningful difference between a "fresh start" and reusing the conda installation is a rmtree(). * Exposes STASIS_DOWNLOAD_TIMEOUT environment variable * Sets the connection timeout for libcurl to 30, instead of 300. * Export ini_section_create() function * Add download() tests * Add conda_*() tests * Add boilerplate source file for test framework * Fixes segfault reported by @GeorgeJCleary (#10) * The key is now an array index. When key is -1, the env variable is not defined. * Free resources only when continue on error is disabled (#11) * Fix segfault due to premature shutdown/cleanup * If conda_setup_headless cannot succeed, die * Set STASIS_SYSCONFDIR for tests
-rw-r--r--.github/workflows/cmake-multi-platform.yml2
-rw-r--r--include/conda.h2
-rw-r--r--include/ini.h8
-rw-r--r--src/conda.c17
-rw-r--r--src/delivery.c21
-rw-r--r--src/download.c10
-rw-r--r--tests/_test_boilerplate.c17
-rw-r--r--tests/test_conda.c181
-rw-r--r--tests/test_download.c50
9 files changed, 300 insertions, 8 deletions
diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml
index 6d4709a..e7d4b33 100644
--- a/.github/workflows/cmake-multi-platform.yml
+++ b/.github/workflows/cmake-multi-platform.yml
@@ -56,3 +56,5 @@ jobs:
- 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
+ env:
+ STASIS_SYSCONFDIR: ../../..
diff --git a/include/conda.h b/include/conda.h
index cea3f02..d439371 100644
--- a/include/conda.h
+++ b/include/conda.h
@@ -89,7 +89,7 @@ int conda_activate(const char *root, const char *env_name);
/**
* Configure the active conda installation for headless operation
*/
-void conda_setup_headless();
+int conda_setup_headless();
/**
* Creates a Conda environment from a YAML config
diff --git a/include/ini.h b/include/ini.h
index af2639b..7167cad 100644
--- a/include/ini.h
+++ b/include/ini.h
@@ -101,6 +101,14 @@ struct INIFILE *ini_open(const char *filename);
struct INISection *ini_section_search(struct INIFILE **ini, unsigned mode, const char *value);
/**
+ *
+ * @param ini
+ * @param key
+ * @return
+ */
+int ini_section_create(struct INIFILE **ini, char *key);
+
+/**
*
* @param ini
* @param section
diff --git a/src/conda.c b/src/conda.c
index 976bbbc..7aaec77 100644
--- a/src/conda.c
+++ b/src/conda.c
@@ -35,7 +35,10 @@ int micromamba(struct MicromambaInfo *info, char *command, ...) {
char untarcmd[PATH_MAX * 2];
mkdirs(info->micromamba_prefix, 0755);
sprintf(untarcmd, "tar -xvf %s -C %s --strip-components=1 bin/micromamba 1>/dev/null", installer_path, info->micromamba_prefix);
- system(untarcmd);
+ int untarcmd_status = system(untarcmd);
+ if (untarcmd_status) {
+ return -1;
+ }
}
char cmd[STASIS_BUFSIZ];
@@ -249,7 +252,7 @@ int conda_check_required() {
return 0;
}
-void conda_setup_headless() {
+int conda_setup_headless() {
if (globals.verbose) {
conda_exec("config --system --set quiet false");
} else {
@@ -285,7 +288,7 @@ void conda_setup_headless() {
if (conda_exec(cmd)) {
msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "Unable to install user-defined base packages (conda)\n");
- exit(1);
+ return 1;
}
}
@@ -307,7 +310,7 @@ void conda_setup_headless() {
if (pip_exec(cmd)) {
msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "Unable to install user-defined base packages (pip)\n");
- exit(1);
+ return 1;
}
}
@@ -315,15 +318,17 @@ void conda_setup_headless() {
msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "Your STASIS configuration lacks the bare"
" minimum software required to build conda packages."
" Please fix it.\n");
- exit(1);
+ return 1;
}
if (globals.always_update_base_environment) {
if (conda_exec("update --all")) {
fprintf(stderr, "conda update was unsuccessful\n");
- exit(1);
+ return 1;
}
}
+
+ return 0;
}
int conda_env_create_from_uri(char *name, char *uri) {
diff --git a/src/delivery.c b/src/delivery.c
index 0c20550..b27ab08 100644
--- a/src/delivery.c
+++ b/src/delivery.c
@@ -420,6 +420,10 @@ static int populate_mission_ini(struct Delivery **ctx) {
}
void validate_delivery_ini(struct INIFILE *ini) {
+ if (!ini) {
+ SYSERROR("%s", "INIFILE is NULL!");
+ exit(1);
+ }
if (ini_section_search(&ini, INI_SEARCH_EXACT, "meta")) {
ini_has_key_required(ini, "meta", "name");
ini_has_key_required(ini, "meta", "version");
@@ -1428,6 +1432,18 @@ void delivery_install_conda(char *install_script, char *conda_install_dir) {
fprintf(stderr, "conda installation failed\n");
exit(1);
}
+ } else {
+ // Proceed with the installation
+ // -b = batch mode (non-interactive)
+ char cmd[PATH_MAX] = {0};
+ snprintf(cmd, sizeof(cmd) - 1, "%s %s -b -p %s",
+ find_program("bash"),
+ install_script,
+ conda_install_dir);
+ if (shell_safe(&proc, cmd)) {
+ fprintf(stderr, "conda installation failed\n");
+ exit(1);
+ }
}
} else {
msg(STASIS_MSG_L3, "Conda removal disabled by configuration\n");
@@ -1451,7 +1467,10 @@ void delivery_conda_enable(struct Delivery *ctx, char *conda_install_dir) {
exit(1);
}
- conda_setup_headless();
+ if (conda_setup_headless()) {
+ // no COE check. this call must succeed.
+ exit(1);
+ }
}
void delivery_defer_packages(struct Delivery *ctx, int type) {
diff --git a/src/download.c b/src/download.c
index 1623560..f83adda 100644
--- a/src/download.c
+++ b/src/download.c
@@ -3,6 +3,7 @@
//
#include <string.h>
+#include <stdlib.h>
#include "download.h"
size_t download_writer(void *fp, size_t size, size_t nmemb, void *stream) {
@@ -18,6 +19,8 @@ long download(char *url, const char *filename, char **errmsg) {
FILE *fp;
char user_agent[20];
sprintf(user_agent, "stasis/%s", VERSION);
+ long timeout = 30L;
+ char *timeout_str = getenv("STASIS_DOWNLOAD_TIMEOUT");
curl_global_init(CURL_GLOBAL_ALL);
c = curl_easy_init();
@@ -27,11 +30,18 @@ long download(char *url, const char *filename, char **errmsg) {
if (!fp) {
return -1;
}
+
curl_easy_setopt(c, CURLOPT_VERBOSE, 0L);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_USERAGENT, user_agent);
curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(c, CURLOPT_WRITEDATA, fp);
+
+ if (timeout_str) {
+ timeout = strtol(timeout_str, NULL, 10);
+ }
+ curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, timeout);
+
curl_code = curl_easy_perform(c);
if (curl_code != CURLE_OK) {
if (errmsg) {
diff --git a/tests/_test_boilerplate.c b/tests/_test_boilerplate.c
new file mode 100644
index 0000000..1d7734b
--- /dev/null
+++ b/tests/_test_boilerplate.c
@@ -0,0 +1,17 @@
+#include "testing.h"
+
+void test_NAME() {
+ struct testcase {
+
+ };
+ STASIS_ASSERT();
+}
+
+int main(int argc, char *argv[]) {
+ STASIS_TEST_BEGIN_MAIN();
+ STASIS_TEST_FUNC *tests[] = {
+ test_NAME,
+ };
+ STASIS_TEST_RUN(tests);
+ STASIS_TEST_END_MAIN();
+} \ No newline at end of file
diff --git a/tests/test_conda.c b/tests/test_conda.c
new file mode 100644
index 0000000..49cf48b
--- /dev/null
+++ b/tests/test_conda.c
@@ -0,0 +1,181 @@
+#include "testing.h"
+
+char cwd_start[PATH_MAX];
+char cwd_workspace[PATH_MAX];
+int conda_is_installed = 0;
+
+void test_micromamba() {
+ char mm_prefix[PATH_MAX] = {0};
+ char c_prefix[PATH_MAX] = {0};
+ snprintf(mm_prefix, strlen(cwd_workspace) + strlen("micromamba") + 2, "%s/%s", cwd_workspace, "micromamba");
+ snprintf(c_prefix, strlen(mm_prefix) + strlen("conda") + 2, "%s/%s", mm_prefix, "conda");
+
+ struct testcase {
+ struct MicromambaInfo mminfo;
+ const char *cmd;
+ int result;
+ };
+ struct testcase tc[] = {
+ {.mminfo = {.micromamba_prefix = mm_prefix, .conda_prefix = c_prefix}, .cmd = "info", .result = 0},
+ {.mminfo = {.micromamba_prefix = mm_prefix, .conda_prefix = c_prefix}, .cmd = "env list", .result = 0},
+ {.mminfo = {.micromamba_prefix = mm_prefix, .conda_prefix = c_prefix}, .cmd = "run python -V", .result = 0},
+ {.mminfo = {.micromamba_prefix = mm_prefix, .conda_prefix = c_prefix}, .cmd = "no_such_option", .result = 109},
+ };
+
+ for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) {
+ struct testcase *item = &tc[i];
+ int result = micromamba(&item->mminfo, (char *) item->cmd);
+ if (result > 0) {
+ result = result >> 8;
+ }
+ STASIS_ASSERT(result == item->result, "unexpected exit value");
+ SYSERROR("micromamba command: '%s' (returned: %d)", item->cmd, result);
+ }
+}
+
+static char conda_prefix[PATH_MAX] = {0};
+struct Delivery ctx;
+
+void test_conda_installation() {
+ char *install_url = calloc(255, sizeof(install_url));
+ delivery_get_installer_url(&ctx, install_url);
+ delivery_get_installer(&ctx, install_url);
+ delivery_install_conda(ctx.conda.installer_path, ctx.storage.conda_install_prefix);
+ STASIS_ASSERT_FATAL(access(ctx.storage.conda_install_prefix, F_OK) == 0, "conda was not installed correctly");
+ STASIS_ASSERT_FATAL(conda_activate(ctx.storage.conda_install_prefix, "base") == 0, "unable to activate base environment");
+ STASIS_ASSERT_FATAL(conda_exec("info") == 0, "conda was not installed correctly");
+ conda_is_installed = 1;
+}
+
+void test_conda_activate() {
+ struct testcase {
+ const char *key;
+ char *value;
+ };
+ struct testcase tc[] = {
+ {.key = "CONDA_DEFAULT_ENV", .value = "base"},
+ {.key = "CONDA_SHLVL", .value = "1"},
+ {.key = "_CE_CONDA", ""},
+ {.key = "_CE_M", ""},
+ {.key = "CONDA_PREFIX", ctx.storage.conda_install_prefix},
+ };
+
+ STASIS_SKIP_IF(!conda_is_installed, "cannot run without conda");
+ STASIS_ASSERT_FATAL(conda_activate(ctx.storage.conda_install_prefix, "base") == 0, "unable to activate base environment");
+
+ for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) {
+ struct testcase *item = &tc[i];
+ char *value = getenv(item->key);
+ STASIS_ASSERT(value != NULL, "expected key to exist, but doesn't. conda is not activated.");
+ if (value) {
+ STASIS_ASSERT(strcmp(value, item->value) == 0, "conda variable value mismatch");
+ }
+ }
+}
+
+void test_conda_exec() {
+ STASIS_ASSERT(conda_exec("--version") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("info") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("search fitsverify") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("create -n testenv") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("install -n testenv fitsverify") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("update -n testenv fitsverify") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("list -n testenv") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("run -n testenv fitsverify") == 0, "conda is broken");
+ STASIS_ASSERT(conda_exec("notasubcommand --help") != 0, "conda is broken");
+}
+
+void test_python_exec() {
+ const char *python_system_path = find_program("python3");
+ char python_path[PATH_MAX];
+ sprintf(python_path, "%s/bin/python3", ctx.storage.conda_install_prefix);
+
+ STASIS_ASSERT(strcmp(python_path, python_system_path ? python_system_path : "/not/found") == 0, "conda is not configured correctly.");
+ STASIS_ASSERT(python_exec("-V") == 0, "python is broken");
+}
+
+void test_conda_setup_headless() {
+ globals.conda_packages = strlist_init();
+ globals.pip_packages = strlist_init();
+ strlist_append(&globals.conda_packages, "boa");
+ strlist_append(&globals.conda_packages, "conda-build");
+ strlist_append(&globals.conda_packages, "conda-verify");
+ strlist_append(&globals.pip_packages, "pytest");
+ STASIS_ASSERT(conda_setup_headless() == 0, "headless configuration failed");
+}
+
+void test_conda_env_create_from_uri() {
+ const char *url = "https://ssb.stsci.edu/jhunk/stasis_test/test_conda_env_create_from_uri.yml";
+ char *name = strdup(__FUNCTION__);
+ STASIS_ASSERT(conda_env_create_from_uri(name, (char *) url) == 0, "creating an environment from a remote source failed");
+ free(name);
+}
+
+void test_conda_env_create_export_remove() {
+ char *name = strdup(__FUNCTION__);
+ STASIS_ASSERT(conda_env_create(name, "3", "fitsverify") == 0, "unable to create a simple environment");
+ STASIS_ASSERT(conda_env_export(name, ".", name) == 0, "unable to export an environment");
+ STASIS_ASSERT(conda_env_remove(name) == 0, "unable to remove an environment");
+ free(name);
+}
+
+void test_conda_index() {
+ mkdirs("channel/noarch", 0755);
+ STASIS_ASSERT(conda_index("channel") == 0, "cannot index a simple conda channel");
+}
+
+int main(int argc, char *argv[]) {
+ STASIS_TEST_BEGIN_MAIN();
+ STASIS_TEST_FUNC *tests[] = {
+ test_micromamba,
+ test_conda_installation,
+ test_conda_activate,
+ test_conda_setup_headless,
+ test_conda_exec,
+ test_python_exec,
+ test_conda_env_create_from_uri,
+ test_conda_env_create_export_remove,
+ test_conda_index,
+ };
+
+ const char *ws = "workspace";
+ getcwd(cwd_start, sizeof(cwd_start) - 1);
+ mkdir(ws, 0755);
+ chdir(ws);
+ getcwd(cwd_workspace, sizeof(cwd_workspace) - 1);
+
+ snprintf(conda_prefix, strlen(cwd_workspace) + strlen("conda") + 2, "%s/conda", cwd_workspace);
+
+ const char *mockinidata = "[meta]\n"
+ "name = mock\n"
+ "version = 1.0.0\n"
+ "rc = 1\n"
+ "mission = generic\n"
+ "python = 3.11\n"
+ "[conda]\n"
+ "installer_name = Miniforge3\n"
+ "installer_version = 24.3.0-0\n"
+ "installer_platform = {{env:STASIS_CONDA_PLATFORM}}\n"
+ "installer_arch = {{env:STASIS_CONDA_ARCH}}\n"
+ "installer_baseurl = https://github.com/conda-forge/miniforge/releases/download/24.3.0-0\n";
+ stasis_testing_write_ascii("mock.ini", mockinidata);
+ struct INIFILE *ini = ini_open("mock.ini");
+ ctx._stasis_ini_fp.delivery = ini;
+ ctx._stasis_ini_fp.delivery_path = realpath("mock.ini", NULL);
+
+ setenv("TMPDIR", cwd_workspace, 1);
+ globals.sysconfdir = getenv("STASIS_SYSCONFDIR");
+ ctx.storage.root = strdup(cwd_workspace);
+
+ setenv("LANG", "C", 1);
+ bootstrap_build_info(&ctx);
+ delivery_init(&ctx);
+
+ STASIS_TEST_RUN(tests);
+
+ chdir(cwd_start);
+ if (rmtree(cwd_workspace)) {
+ perror(cwd_workspace);
+ }
+ STASIS_TEST_END_MAIN();
+} \ No newline at end of file
diff --git a/tests/test_download.c b/tests/test_download.c
new file mode 100644
index 0000000..cee7683
--- /dev/null
+++ b/tests/test_download.c
@@ -0,0 +1,50 @@
+#include "testing.h"
+
+void test_download() {
+ struct testcase {
+ const char *url;
+ long http_code;
+ const char *data;
+ const char *errmsg;
+ };
+ struct testcase tc[] = {
+ {.url = "https://ssb.stsci.edu/jhunk/stasis_test/test_download.txt", .http_code = 200L, .data = "It works!\n", .errmsg = NULL},
+ {.url = "https://ssb.stsci.edu/jhunk/stasis_test/test_download.broken", .http_code = 404L, .data = "<html", .errmsg = NULL},
+ {.url = "https://example.tld", .http_code = -1L, .data = NULL, .errmsg = "Couldn't resolve host name"},
+ };
+
+ for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) {
+ const char *filename = "output.txt";
+ char errmsg[BUFSIZ] = {0};
+ char *errmsg_p = errmsg;
+ long http_code = download((char *) tc[i].url, filename, &errmsg_p);
+ if (tc[i].errmsg) {
+ STASIS_ASSERT(strlen(errmsg_p), "an error should have been thrown by curl, but wasn't");
+ fflush(stderr);
+ SYSERROR("curl error message: %s", errmsg_p);
+ } else {
+ STASIS_ASSERT(!strlen(errmsg_p), "unexpected error thrown by curl");
+ }
+ STASIS_ASSERT(http_code == tc[i].http_code, "expecting non-error HTTP code");
+
+ char **data = file_readlines(filename, 0, 100, NULL);
+ if (http_code >= 0) {
+ STASIS_ASSERT(data != NULL, "data should not be null");
+ STASIS_ASSERT(strncmp(data[0], tc[i].data, strlen(tc[i].data)) == 0, "data file does not match the expected contents");
+ } else {
+ STASIS_ASSERT(http_code == -1, "http_code should be -1 on fatal curl error");
+ STASIS_ASSERT(data == NULL, "data should be NULL on fatal curl error");
+ }
+ guard_free(data);
+ remove(filename);
+ }
+}
+
+int main(int argc, char *argv[]) {
+ STASIS_TEST_BEGIN_MAIN();
+ STASIS_TEST_FUNC *tests[] = {
+ test_download,
+ };
+ STASIS_TEST_RUN(tests);
+ STASIS_TEST_END_MAIN();
+} \ No newline at end of file