From 1b8b454ccde84efeb568c8ffb8937581fe3c3e9c Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Thu, 23 Nov 2023 13:31:54 -0700 Subject: [PATCH] Correctly handle nonexistent `spec.json` file. --- include/config.h | 6 ++-- include/loader.h | 25 ++++++++++++++ include/path.h | 8 +++++ main.c | 42 ++++++++++++++++++---- src/loader.c | 48 ++++++++++++++++++++++++++ src/path.c | 28 +++++++++++++++ test/runner.c | 9 +++++ test/specs/minimal_spec_json/run.sh | 3 ++ test/specs/minimal_spec_json/spec.json | 1 + test/specs/no_spec_json/run.sh | 3 ++ test/test_config.h | 13 +++++++ test/test_dyn_array.h | 6 ++++ test/test_loader.h | 37 ++++++++++++++++++++ test/test_path.h | 21 +++++++++++ 14 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 include/loader.h create mode 100644 include/path.h create mode 100644 src/loader.c create mode 100644 src/path.c create mode 100644 test/specs/minimal_spec_json/run.sh create mode 100644 test/specs/minimal_spec_json/spec.json create mode 100644 test/specs/no_spec_json/run.sh create mode 100644 test/test_loader.h create mode 100644 test/test_path.h diff --git a/include/config.h b/include/config.h index 31d277d..6c9e6a4 100644 --- a/include/config.h +++ b/include/config.h @@ -14,11 +14,11 @@ struct Config { }; enum ConfigError { - // Indicates the $CWD could not be retrieved. + // The $CWD could not be retrieved. CE_ENV_CWD_INVALID = 1, - // Indicates the $BOOTSTRAP_ROOT_DIR environment variable is empty. + // The $BOOTSTRAP_ROOT_DIR environment variable is empty. CE_ENV_ROOT_DIR_INVALID, - // Indicates the target argument is invalid. + // The target argument is invalid. CE_TARGET_INVALID, }; diff --git a/include/loader.h b/include/loader.h new file mode 100644 index 0000000..fd734ca --- /dev/null +++ b/include/loader.h @@ -0,0 +1,25 @@ +#ifndef _BOOTSTRAP_LOADER_H +#define _BOOTSTRAP_LOADER_H + +#include "cJSON.h" +#include "config.h" + +enum SpecJsonError { + // The `spec.json` file exists but cannot be open. + SJE_JSON_CANNOT_OPEN = 1, + // The JSON of the corresponding file is not syntactically valid JSON. + SJE_JSON_INVALID, +}; + +/* +Reads in the `spec.json` file relative to the paths of the provided @Config. + +A spec directory does not necessarily contain a `spec.json` file. If this file +cannot be found, the @parsed pointer is set to NULL with a success return code. + +@return: 0 on success and a @SpecJsonError otherwise. +*/ +enum SpecJsonError +read_spec_json(const struct Config *const config, cJSON **parsed); + +#endif /* _BOOTSTRAP_LOADER_H */ diff --git a/include/path.h b/include/path.h new file mode 100644 index 0000000..c83f673 --- /dev/null +++ b/include/path.h @@ -0,0 +1,8 @@ +#ifndef _BOOTSTRAP_PATH_H +#define _BOOTSTRAP_PATH_H + +#include + +char *join_path_segments(size_t n, const char *segments[static n]); + +#endif /* _BOOTSTRAP_PATH_H */ diff --git a/main.c b/main.c index 46ee62f..12e37be 100644 --- a/main.c +++ b/main.c @@ -2,8 +2,9 @@ #include #include +#include "cJSON.h" #include "config.h" -#include "dyn_array.h" +#include "loader.h" const char *ENV_BOOTSTRAP_ROOT_DIR = "BOOTSTRAP_ROOT_DIR"; @@ -15,24 +16,51 @@ int main(int argc, char **argv) { exit(EXIT_FAILURE); } - const char *cwd = getcwd(0, 0); + int retval = EXIT_SUCCESS; + char *cwd = getcwd(0, 0); const char *root_dir = getenv(ENV_BOOTSTRAP_ROOT_DIR); const char *target = argv[1]; + // `cwd` must be free'd. + struct Config *config = 0; switch (config_load(cwd, root_dir, target, &config)) { case CE_ENV_CWD_INVALID: fprintf(stderr, "Could not retrieve the $CWD value."); - exit(EXIT_FAILURE); + retval = EXIT_FAILURE; + goto cwd_cleanup; case CE_ENV_ROOT_DIR_INVALID: fprintf(stderr, "Must specify $BOOTSTRAP_ROOT_DIR environment variable."); - exit(EXIT_FAILURE); + retval = EXIT_FAILURE; + goto cwd_cleanup; case CE_TARGET_INVALID: fprintf(stderr, "Target spec `%s` is invalid.", argv[1]); - exit(EXIT_FAILURE); + retval = EXIT_FAILURE; + goto cwd_cleanup; } + // `config` must be free'd. + + cJSON *parsed = 0; + switch (read_spec_json(config, &parsed)) { + case SJE_JSON_CANNOT_OPEN: + fprintf(stderr, "Found `spec.json` but could not open."); + retval = EXIT_FAILURE; + goto config_cleanup; + case SJE_JSON_INVALID: + fprintf(stderr, "`spec.json` does not conform to bootstrap format."); + retval = EXIT_FAILURE; + goto config_cleanup; + } + + // TODO: Extract the prompts out of the `spec.json` file. + // TODO: Load in the curses interface. + // TODO: Run `run.sh`. + +config_cleanup: config_free(config); - free((void *)cwd); - return EXIT_SUCCESS; + +cwd_cleanup: + free(cwd); + return retval; } diff --git a/src/loader.c b/src/loader.c new file mode 100644 index 0000000..fc2a360 --- /dev/null +++ b/src/loader.c @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +#include "loader.h" +#include "path.h" + +/* +Returns a file pointer to the `spec.json` file if it exists. Returns 0 +otherwise. +*/ +static int find_spec_json(const struct Config *const config, FILE **handle) { + const char *segments[] = {config->root_dir, config->target, "spec.json"}; + char *filepath = + join_path_segments(sizeof(segments) / sizeof(char *), segments); + + int retval = 0; + // It is ok if the file does not exist. It is not ok if we couldn't open the + // file for any other reason. + *handle = fopen(filepath, "r"); + if (!*handle && errno != ENOENT) { + retval = errno; + } + + free(filepath); + return retval; +} + +enum SpecJsonError +read_spec_json(const struct Config *const config, cJSON **parsed) { + FILE *handle = 0; + int retval = find_spec_json(config, &handle); + + if (retval != 0) { + return SJE_JSON_CANNOT_OPEN; + } + + // The `spec.json` file does not exist. + if (!handle) { + *parsed = 0; + return 0; + } + + // TODO: Need to parse the spec.json file. + fclose(handle); + assert(false); +} diff --git a/src/path.c b/src/path.c new file mode 100644 index 0000000..d5056dd --- /dev/null +++ b/src/path.c @@ -0,0 +1,28 @@ +#include +#include + +#include "path.h" + +char *join_path_segments(size_t n, const char *segments[static n]) { + assert(n > 0); + + size_t length = 0; + for (int i = 0; i < n; ++i) { + length += strlen(segments[i]); + } + length += n - 1; // Path separators. + + size_t offset = 0; + char *joined = calloc(1, length + 1); + + for (int i = 0; i < n; ++i) { + const size_t segment_len = strlen(segments[i]); + memcpy(joined + offset, segments[i], segment_len); + offset += segment_len; + if (i < n - 1) { + joined[offset++] = '/'; + } + } + + return joined; +} diff --git a/test/runner.c b/test/runner.c index 0f6b296..e512b5e 100644 --- a/test/runner.c +++ b/test/runner.c @@ -1,6 +1,8 @@ #include "sput.h" #include "test_config.h" #include "test_dyn_array.h" +#include "test_loader.h" +#include "test_path.h" int main(int argc, char *argv[]) { sput_start_testing(); @@ -14,6 +16,13 @@ int main(int argc, char *argv[]) { sput_run_test(test_dyn_array_empty); sput_run_test(test_dyn_array_nonzero_capacity); + sput_enter_suite("path"); + sput_run_test(test_join_path_single_segments); + sput_run_test(test_join_path_multiple_segments); + + sput_enter_suite("loader"); + sput_run_test(test_read_spec_json_missing); + sput_finish_testing(); return sput_get_return_value(); diff --git a/test/specs/minimal_spec_json/run.sh b/test/specs/minimal_spec_json/run.sh new file mode 100644 index 0000000..c914ca6 --- /dev/null +++ b/test/specs/minimal_spec_json/run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "hello world" diff --git a/test/specs/minimal_spec_json/spec.json b/test/specs/minimal_spec_json/spec.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/specs/minimal_spec_json/spec.json @@ -0,0 +1 @@ +{} diff --git a/test/specs/no_spec_json/run.sh b/test/specs/no_spec_json/run.sh new file mode 100644 index 0000000..c914ca6 --- /dev/null +++ b/test/specs/no_spec_json/run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "hello world" diff --git a/test/test_config.h b/test/test_config.h index b6ad262..d726c10 100644 --- a/test/test_config.h +++ b/test/test_config.h @@ -8,6 +8,9 @@ static const char *SAMPLE_CWD = "/home/jrpotter/Documents/bootstrap"; static const char *SAMPLE_ROOT_DIR = "/usr/local/share/specs"; static const char *SAMPLE_TARGET = "example-target"; +/* +A valid $CWD environment variable value must be supplied. +*/ static void test_config_load_cwd_invalid() { struct Config *config = 0; enum ConfigError retval = @@ -15,12 +18,18 @@ static void test_config_load_cwd_invalid() { sput_fail_unless(retval == CE_ENV_CWD_INVALID, "target == 0"); } +/* +A valid $BOOTSTRAP_ROOT_DIR environment variable value must be supplied. +*/ static void test_config_load_root_dir_invalid() { struct Config *config = 0; enum ConfigError retval = config_load(SAMPLE_CWD, 0, SAMPLE_TARGET, &config); sput_fail_unless(retval == CE_ENV_ROOT_DIR_INVALID, "root_dir == 0"); } +/* +A valid target value must be supplied. +*/ static void test_config_load_target_invalid() { struct Config *config = 0; enum ConfigError retval = @@ -28,6 +37,10 @@ static void test_config_load_target_invalid() { sput_fail_unless(retval == CE_TARGET_INVALID, "target == 0"); } +/* +If the supplied arguments to `config_load` are non-NULL, we should be able to +successfully construct a new @Config instance. +*/ static void test_config_load_success() { struct Config *config = 0; enum ConfigError retval = diff --git a/test/test_dyn_array.h b/test/test_dyn_array.h index f4b9558..9cdb115 100644 --- a/test/test_dyn_array.h +++ b/test/test_dyn_array.h @@ -4,6 +4,9 @@ #include "dyn_array.h" #include "sput.h" +/* +A @DynArray with zero capacity can be instantiated and have entries pushed onto. +*/ static void test_dyn_array_empty() { struct DynArray *a = dyn_array_new(0); sput_fail_unless(a->size == 0, "a->size == 0"); @@ -22,6 +25,9 @@ static void test_dyn_array_empty() { dyn_array_free(a); } +/* +A @DynArray with nonzero capacity can be instantiated and have entries pushed onto. +*/ static void test_dyn_array_nonzero_capacity() { struct DynArray *a = dyn_array_new(3); sput_fail_unless(a->size == 0, "a->size == 0"); diff --git a/test/test_loader.h b/test/test_loader.h new file mode 100644 index 0000000..9d94100 --- /dev/null +++ b/test/test_loader.h @@ -0,0 +1,37 @@ +#ifndef _BOOTSTRAP_TEST_LOADER +#define _BOOTSTRAP_TEST_LOADER + +#include + +#include "cJSON.h" +#include "config.h" +#include "loader.h" +#include "path.h" +#include "sput.h" + +/* +A missing `spec.json` file is not an error. Our parsed @cJSON instance should +be set to NULL in this case. +*/ +static void test_read_spec_json_missing() { + char *cwd = getcwd(0, 0); + + const char *segments[] = {cwd, "test", "specs"}; + char *root_dir = join_path_segments(sizeof(segments) / sizeof(char *), segments); + + struct Config config = { + cwd, + root_dir, + "no_spec_json", + }; + + cJSON *parsed = 0; + enum SpecJsonError retval = read_spec_json(&config, &parsed); + sput_fail_unless(retval == 0, "no spec.json, success"); + sput_fail_unless(parsed == 0, "no spec.json, no parsed"); + + free(cwd); + free(root_dir); +} + +#endif /* _BOOTSTRAP_TEST_LOADER */ diff --git a/test/test_path.h b/test/test_path.h new file mode 100644 index 0000000..6f6d362 --- /dev/null +++ b/test/test_path.h @@ -0,0 +1,21 @@ +#ifndef _BOOTSTRAP_TEST_PATH +#define _BOOTSTRAP_TEST_PATH + +#include "path.h" +#include "sput.h" + +static void test_join_path_single_segments() { + const char *segments[] = {"abc"}; + char *joined = join_path_segments(sizeof(segments) / sizeof(char *), segments); + sput_fail_unless(strcmp(joined, "abc") == 0, "abc"); + free(joined); +} + +static void test_join_path_multiple_segments() { + const char *segments[] = {"abc", "def", "ghi"}; + char *joined = join_path_segments(sizeof(segments) / sizeof(char *), segments); + sput_fail_unless(strcmp(joined, "abc/def/ghi") == 0, "abc/def/ghi"); + free(joined); +} + +#endif /* _BOOTSTRAP_TEST_PATH */