Correctly handle nonexistent `spec.json` file.

pull/9/head
Joshua Potter 2023-11-23 13:31:54 -07:00
parent 694fe98167
commit 1b8b454ccd
14 changed files with 240 additions and 10 deletions

View File

@ -14,11 +14,11 @@ struct Config {
}; };
enum ConfigError { enum ConfigError {
// Indicates the $CWD could not be retrieved. // The $CWD could not be retrieved.
CE_ENV_CWD_INVALID = 1, 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, CE_ENV_ROOT_DIR_INVALID,
// Indicates the target argument is invalid. // The target argument is invalid.
CE_TARGET_INVALID, CE_TARGET_INVALID,
}; };

25
include/loader.h Normal file
View File

@ -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 */

8
include/path.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef _BOOTSTRAP_PATH_H
#define _BOOTSTRAP_PATH_H
#include <stdlib.h>
char *join_path_segments(size_t n, const char *segments[static n]);
#endif /* _BOOTSTRAP_PATH_H */

42
main.c
View File

@ -2,8 +2,9 @@
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include "cJSON.h"
#include "config.h" #include "config.h"
#include "dyn_array.h" #include "loader.h"
const char *ENV_BOOTSTRAP_ROOT_DIR = "BOOTSTRAP_ROOT_DIR"; const char *ENV_BOOTSTRAP_ROOT_DIR = "BOOTSTRAP_ROOT_DIR";
@ -15,24 +16,51 @@ int main(int argc, char **argv) {
exit(EXIT_FAILURE); 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 *root_dir = getenv(ENV_BOOTSTRAP_ROOT_DIR);
const char *target = argv[1]; const char *target = argv[1];
// `cwd` must be free'd.
struct Config *config = 0; struct Config *config = 0;
switch (config_load(cwd, root_dir, target, &config)) { switch (config_load(cwd, root_dir, target, &config)) {
case CE_ENV_CWD_INVALID: case CE_ENV_CWD_INVALID:
fprintf(stderr, "Could not retrieve the $CWD value."); fprintf(stderr, "Could not retrieve the $CWD value.");
exit(EXIT_FAILURE); retval = EXIT_FAILURE;
goto cwd_cleanup;
case CE_ENV_ROOT_DIR_INVALID: case CE_ENV_ROOT_DIR_INVALID:
fprintf(stderr, "Must specify $BOOTSTRAP_ROOT_DIR environment variable."); fprintf(stderr, "Must specify $BOOTSTRAP_ROOT_DIR environment variable.");
exit(EXIT_FAILURE); retval = EXIT_FAILURE;
goto cwd_cleanup;
case CE_TARGET_INVALID: case CE_TARGET_INVALID:
fprintf(stderr, "Target spec `%s` is invalid.", argv[1]); 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); config_free(config);
free((void *)cwd);
return EXIT_SUCCESS; cwd_cleanup:
free(cwd);
return retval;
} }

48
src/loader.c Normal file
View File

@ -0,0 +1,48 @@
#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#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);
}

28
src/path.c Normal file
View File

@ -0,0 +1,28 @@
#include <assert.h>
#include <string.h>
#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;
}

View File

@ -1,6 +1,8 @@
#include "sput.h" #include "sput.h"
#include "test_config.h" #include "test_config.h"
#include "test_dyn_array.h" #include "test_dyn_array.h"
#include "test_loader.h"
#include "test_path.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
sput_start_testing(); 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_empty);
sput_run_test(test_dyn_array_nonzero_capacity); 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(); sput_finish_testing();
return sput_get_return_value(); return sput_get_return_value();

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "hello world"

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "hello world"

View File

@ -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_ROOT_DIR = "/usr/local/share/specs";
static const char *SAMPLE_TARGET = "example-target"; static const char *SAMPLE_TARGET = "example-target";
/*
A valid $CWD environment variable value must be supplied.
*/
static void test_config_load_cwd_invalid() { static void test_config_load_cwd_invalid() {
struct Config *config = 0; struct Config *config = 0;
enum ConfigError retval = enum ConfigError retval =
@ -15,12 +18,18 @@ static void test_config_load_cwd_invalid() {
sput_fail_unless(retval == CE_ENV_CWD_INVALID, "target == 0"); 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() { static void test_config_load_root_dir_invalid() {
struct Config *config = 0; struct Config *config = 0;
enum ConfigError retval = config_load(SAMPLE_CWD, 0, SAMPLE_TARGET, &config); enum ConfigError retval = config_load(SAMPLE_CWD, 0, SAMPLE_TARGET, &config);
sput_fail_unless(retval == CE_ENV_ROOT_DIR_INVALID, "root_dir == 0"); 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() { static void test_config_load_target_invalid() {
struct Config *config = 0; struct Config *config = 0;
enum ConfigError retval = enum ConfigError retval =
@ -28,6 +37,10 @@ static void test_config_load_target_invalid() {
sput_fail_unless(retval == CE_TARGET_INVALID, "target == 0"); 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() { static void test_config_load_success() {
struct Config *config = 0; struct Config *config = 0;
enum ConfigError retval = enum ConfigError retval =

View File

@ -4,6 +4,9 @@
#include "dyn_array.h" #include "dyn_array.h"
#include "sput.h" #include "sput.h"
/*
A @DynArray with zero capacity can be instantiated and have entries pushed onto.
*/
static void test_dyn_array_empty() { static void test_dyn_array_empty() {
struct DynArray *a = dyn_array_new(0); struct DynArray *a = dyn_array_new(0);
sput_fail_unless(a->size == 0, "a->size == 0"); sput_fail_unless(a->size == 0, "a->size == 0");
@ -22,6 +25,9 @@ static void test_dyn_array_empty() {
dyn_array_free(a); dyn_array_free(a);
} }
/*
A @DynArray with nonzero capacity can be instantiated and have entries pushed onto.
*/
static void test_dyn_array_nonzero_capacity() { static void test_dyn_array_nonzero_capacity() {
struct DynArray *a = dyn_array_new(3); struct DynArray *a = dyn_array_new(3);
sput_fail_unless(a->size == 0, "a->size == 0"); sput_fail_unless(a->size == 0, "a->size == 0");

37
test/test_loader.h Normal file
View File

@ -0,0 +1,37 @@
#ifndef _BOOTSTRAP_TEST_LOADER
#define _BOOTSTRAP_TEST_LOADER
#include <unistd.h>
#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 */

21
test/test_path.h Normal file
View File

@ -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 */