Add command line parsing and test spec directory existence.

pull/9/head
Joshua Potter 2023-11-24 11:47:23 -07:00
parent aba0439858
commit 795307f2a1
8 changed files with 211 additions and 123 deletions

View File

@ -20,6 +20,10 @@ enum ConfigError {
CE_ENV_ROOT_DIR_INVALID, CE_ENV_ROOT_DIR_INVALID,
// The target argument is invalid. // The target argument is invalid.
CE_TARGET_INVALID, CE_TARGET_INVALID,
// No spec with the given name was found.
CE_TARGET_NOT_FOUND,
// The spec is not a directory.
CE_TARGET_NOT_DIR,
}; };
enum ConfigError config_load( enum ConfigError config_load(

106
main.c
View File

@ -1,3 +1,4 @@
#include <ctype.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
@ -7,76 +8,99 @@
#include "parser.h" #include "parser.h"
#include "validator.h" #include "validator.h"
const char *ENV_BOOTSTRAP_ROOT_DIR = "BOOTSTRAP_ROOT_DIR"; static int run(const char *root_dir, const char *target) {
int retval = EXIT_FAILURE;
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "Usage: bootstrap <spec>\n");
exit(EXIT_FAILURE);
}
int retval = EXIT_SUCCESS;
char *cwd = getcwd(0, 0); char *cwd = getcwd(0, 0);
const char *root_dir = getenv(ENV_BOOTSTRAP_ROOT_DIR); if (!root_dir) {
const char *target = argv[1]; root_dir = getenv("BOOTSTRAP_ROOT_DIR");
}
// `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 $CWD.\n");
retval = EXIT_FAILURE; goto cleanup_cwd;
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(
retval = EXIT_FAILURE; stderr,
goto cwd_cleanup; "Either supply a value to `-d` or specify the $BOOTSTRAP_ROOT_DIR "
"environment variable.\n"
);
goto cleanup_cwd;
case CE_TARGET_INVALID: case CE_TARGET_INVALID:
fprintf(stderr, "Target spec `%s` is invalid.", argv[1]); fprintf(stderr, "Spec `%s` is invalid.\n", target);
retval = EXIT_FAILURE; goto cleanup_cwd;
goto cwd_cleanup; case CE_TARGET_NOT_FOUND:
fprintf(stderr, "Spec `%s` not found.\n", target);
goto cleanup_cwd;
case CE_TARGET_NOT_DIR:
fprintf(stderr, "Spec `%s` is not a directory.\n", target);
goto cleanup_cwd;
} }
// `config` must be free'd.
cJSON *parsed = 0; cJSON *parsed = 0;
switch (parse_spec_json(config, &parsed)) { switch (parse_spec_json(config, &parsed)) {
case SPE_CANNOT_OPEN: case SPE_CANNOT_OPEN:
fprintf(stderr, "Found `spec.json` but could not open."); fprintf(stderr, "Cannot open `%s/spec.json`.\n", target);
retval = EXIT_FAILURE; goto cleanup_config;
goto config_cleanup;
case SPE_INVALID_SYNTAX: case SPE_INVALID_SYNTAX:
fprintf(stderr, "`spec.json` does not conform to bootstrap format."); fprintf(stderr, "`%s/spec.json` is not valid JSON.\n", target);
retval = EXIT_FAILURE; goto cleanup_config;
goto config_cleanup;
} }
// `parsed` must be free'd.
struct DynArray *prompts = 0; struct DynArray *prompts = 0;
switch (validate_spec_json(parsed, &prompts)) { switch (validate_spec_json(parsed, &prompts)) {
case SVE_NOT_TOPLEVEL_OBJECT: case SVE_NOT_TOPLEVEL_OBJECT:
fprintf(stderr, "`spec.json` top-level JSON value must be object."); fprintf(stderr, "`%s/spec.json` is not a JSON object.\n", target);
retval = EXIT_FAILURE; goto cleanup_parsed;
goto parsed_cleanup;
case SVE_INVALID_VALUE: case SVE_INVALID_VALUE:
fprintf(stderr, "Encountered unknown `spec.json` value type."); fprintf(stderr, "unknown value type found in `%s/spec.json`.\n", target);
retval = EXIT_FAILURE; goto cleanup_parsed;
goto parsed_cleanup;
} }
// TODO: Extract the prompts out of the `spec.json` file. // TODO: Extract the prompts out of the `spec.json` file.
// TODO: Load in the curses interface. // TODO: Load in the curses interface.
// TODO: Run `run.sh`. // TODO: Run `run.sh`.
parsed_cleanup: retval = EXIT_SUCCESS;
cleanup_parsed:
cJSON_Delete(parsed); cJSON_Delete(parsed);
config_cleanup: cleanup_config:
config_free(config); config_free(config);
cwd_cleanup: cleanup_cwd:
free(cwd); free(cwd);
return retval; return retval;
} }
int main(int argc, char **argv) {
char *root_dir = 0;
char *target = 0;
int opt;
while ((opt = getopt(argc, argv, "d:")) != -1) {
switch (opt) {
case 'd':
root_dir = optarg;
break;
}
}
for (int index = optind; index < argc; index++) {
if (target == 0) {
target = argv[index];
} else {
fprintf(stderr, "Usage: bootstrap [-d <ROOT_DIR>] <spec>\n");
return EXIT_FAILURE;
}
}
if (!target) {
fprintf(stderr, "Usage: bootstrap [-d <ROOT_DIR>] <spec>\n");
return EXIT_FAILURE;
}
return run(root_dir, target);
}

View File

@ -1,7 +1,10 @@
#include <errno.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include "config.h" #include "config.h"
#include "path.h"
enum ConfigError config_load( enum ConfigError config_load(
const char *cwd, const char *cwd,
@ -19,6 +22,27 @@ enum ConfigError config_load(
return CE_TARGET_INVALID; return CE_TARGET_INVALID;
} }
{ // Check if the specified directory exists.
struct stat sb;
const char *segments[] = {root_dir, target};
char *filepath =
join_path_segments(sizeof(segments) / sizeof(char *), segments);
int stat_res = stat(filepath, &sb);
free(filepath);
if (stat_res == -1) {
if (errno == ENOENT) {
return CE_TARGET_NOT_FOUND;
}
return CE_TARGET_INVALID;
}
if (!S_ISDIR(sb.st_mode)) {
return CE_TARGET_NOT_DIR;
}
}
*config = malloc(sizeof(struct Config)); *config = malloc(sizeof(struct Config));
(*config)->cwd = cwd; (*config)->cwd = cwd;
(*config)->root_dir = root_dir; (*config)->root_dir = root_dir;

View File

@ -9,12 +9,13 @@ int main(int argc, char *argv[]) {
sput_start_testing(); sput_start_testing();
sput_enter_suite("config"); sput_enter_suite("config");
sput_run_test(test_config_load_root_dir_invalid); sput_run_test(test_config_load_invalid_args);
sput_run_test(test_config_load_target_invalid); sput_run_test(test_config_load_spec_not_found);
sput_run_test(test_config_load_spec_not_dir);
sput_run_test(test_config_load_success); sput_run_test(test_config_load_success);
sput_enter_suite("dyn_array"); sput_enter_suite("dyn_array");
sput_run_test(test_dyn_array_empty); sput_run_test(test_dyn_array_zero_capacity);
sput_run_test(test_dyn_array_nonzero_capacity); sput_run_test(test_dyn_array_nonzero_capacity);
sput_enter_suite("path"); sput_enter_suite("path");

0
test/specs/not_dir Normal file
View File

View File

@ -1,58 +1,94 @@
#ifndef _BOOTSTRAP_TEST_CONFIG #ifndef _BOOTSTRAP_TEST_CONFIG
#define _BOOTSTRAP_TEST_CONFIG #define _BOOTSTRAP_TEST_CONFIG
#include <unistd.h>
#include "config.h" #include "config.h"
#include "path.h"
#include "sput.h" #include "sput.h"
static const char *SAMPLE_CWD = "/home/jrpotter/Documents/bootstrap"; struct TestConfigFixture {
static const char *SAMPLE_ROOT_DIR = "/usr/local/share/specs"; char *cwd;
static const char *SAMPLE_TARGET = "example-target"; char *root_dir;
char *target;
};
static struct TestConfigFixture *test_config_setup() {
char *cwd = getcwd(0, 0);
const char *segments[] = {cwd, "test", "specs"};
char *root_dir =
join_path_segments(sizeof(segments) / sizeof(char *), segments);
struct TestConfigFixture *fixture = malloc(sizeof(struct TestConfigFixture));
fixture->cwd = getcwd(0, 0);
fixture->root_dir = root_dir;
fixture->target = "minimal_spec_json";
return fixture;
}
static void test_config_teardown(struct TestConfigFixture *fixture) {
free(fixture->cwd);
free(fixture->root_dir);
free(fixture);
}
static void test_config_load_invalid_args() {
struct TestConfigFixture *fixture = test_config_setup();
/*
A valid $CWD environment variable value must be supplied.
*/
static void test_config_load_cwd_invalid() {
struct Config *config = 0; struct Config *config = 0;
enum ConfigError retval = enum ConfigError retval = 0;
config_load(0, SAMPLE_ROOT_DIR, SAMPLE_TARGET, &config);
retval = config_load(0, fixture->root_dir, fixture->target, &config);
sput_fail_unless(retval == CE_ENV_CWD_INVALID, "target == 0"); sput_fail_unless(retval == CE_ENV_CWD_INVALID, "target == 0");
} retval = config_load(fixture->cwd, 0, fixture->target, &config);
/*
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"); sput_fail_unless(retval == CE_ENV_ROOT_DIR_INVALID, "root_dir == 0");
} retval = config_load(fixture->cwd, fixture->root_dir, 0, &config);
/*
A valid target value must be supplied.
*/
static void test_config_load_target_invalid() {
struct Config *config = 0;
enum ConfigError retval =
config_load(SAMPLE_CWD, SAMPLE_ROOT_DIR, 0, &config);
sput_fail_unless(retval == CE_TARGET_INVALID, "target == 0"); sput_fail_unless(retval == CE_TARGET_INVALID, "target == 0");
test_config_teardown(fixture);
} }
/* static void test_config_load_spec_not_found() {
If the supplied arguments to `config_load` are non-NULL, we should be able to struct TestConfigFixture *fixture = test_config_setup();
successfully construct a new @Config instance.
*/
static void test_config_load_success() {
struct Config *config = 0; struct Config *config = 0;
enum ConfigError retval = enum ConfigError retval =
config_load(SAMPLE_CWD, SAMPLE_ROOT_DIR, SAMPLE_TARGET, &config); config_load(fixture->cwd, fixture->root_dir, "not_found", &config);
sput_fail_unless(retval == CE_TARGET_NOT_FOUND, "target not found");
test_config_teardown(fixture);
}
static void test_config_load_spec_not_dir() {
struct TestConfigFixture *fixture = test_config_setup();
struct Config *config = 0;
enum ConfigError retval =
config_load(fixture->cwd, fixture->root_dir, "not_dir", &config);
sput_fail_unless(retval == CE_TARGET_NOT_DIR, "target not dir");
test_config_teardown(fixture);
}
static void test_config_load_success() {
struct TestConfigFixture *fixture = test_config_setup();
struct Config *config = 0;
enum ConfigError retval =
config_load(fixture->cwd, fixture->root_dir, fixture->target, &config);
sput_fail_unless(retval == 0, "config_load() success"); sput_fail_unless(retval == 0, "config_load() success");
sput_fail_unless(strcmp(config->root_dir, SAMPLE_ROOT_DIR) == 0, sput_fail_unless(strcmp(config->cwd, fixture->cwd) == 0, "config_load() cwd");
"config_load() root_dir"); sput_fail_unless(
sput_fail_unless(strcmp(config->target, SAMPLE_TARGET) == 0, strcmp(config->root_dir, fixture->root_dir) == 0, "config_load() root_dir"
"config_load() target"); );
sput_fail_unless(
strcmp(config->target, fixture->target) == 0, "config_load() target"
);
config_free(config); config_free(config);
test_config_teardown(fixture);
} }
#endif /* _BOOTSTRAP_TEST_CONFIG */ #endif /* _BOOTSTRAP_TEST_CONFIG */

View File

@ -7,7 +7,7 @@
/* /*
A @DynArray with zero capacity can be instantiated and have entries pushed onto. A @DynArray with zero capacity can be instantiated and have entries pushed onto.
*/ */
static void test_dyn_array_empty() { static void test_dyn_array_zero_capacity() {
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");
sput_fail_unless(a->_capacity == 1, "a->_capacity == 1"); sput_fail_unless(a->_capacity == 1, "a->_capacity == 1");

View File

@ -9,76 +9,75 @@
#include "path.h" #include "path.h"
#include "sput.h" #include "sput.h"
struct TestParserFixture {
char *cwd;
char *root_dir;
const char *target;
struct Config config;
};
static struct TestParserFixture *test_parser_setup(const char *target) {
char *cwd = getcwd(0, 0);
const char *segments[] = {cwd, "test", "specs"};
char *root_dir =
join_path_segments(sizeof(segments) / sizeof(char *), segments);
struct TestParserFixture *fixture = malloc(sizeof(struct TestParserFixture));
fixture->cwd = getcwd(0, 0);
fixture->root_dir = root_dir;
fixture->target = target;
// Reproduce in `Config` instance for convenience.
fixture->config.cwd = fixture->cwd;
fixture->config.root_dir = fixture->root_dir;
fixture->config.target = fixture->target;
return fixture;
}
static void test_parser_teardown(struct TestParserFixture *fixture) {
free(fixture->cwd);
free(fixture->root_dir);
free(fixture);
}
/* /*
A missing `spec.json` file is not an error. Our parsed @cJSON instance should A missing `spec.json` file is not an error. Our parsed @cJSON instance should
be set to NULL in this case. be set to NULL in this case.
*/ */
static void test_parse_spec_json_missing() { static void test_parse_spec_json_missing() {
char *cwd = getcwd(0, 0); struct TestParserFixture *fixture = test_parser_setup("no_spec_json");
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; cJSON *parsed = 0;
enum SpecParseError retval = parse_spec_json(&config, &parsed); enum SpecParseError retval = parse_spec_json(&fixture->config, &parsed);
sput_fail_unless(retval == 0, "no spec.json, success"); sput_fail_unless(retval == 0, "no spec.json, success");
sput_fail_unless(parsed == 0, "no spec.json, no parsed"); sput_fail_unless(parsed == 0, "no spec.json, no parsed");
free(cwd); test_parser_teardown(fixture);
free(root_dir);
} }
static void test_parse_spec_json_minimal() { static void test_parse_spec_json_minimal() {
char *cwd = getcwd(0, 0); struct TestParserFixture *fixture = test_parser_setup("minimal_spec_json");
const char *segments[] = {cwd, "test", "specs"};
char *root_dir =
join_path_segments(sizeof(segments) / sizeof(char *), segments);
struct Config config = {
cwd,
root_dir,
"minimal_spec_json",
};
cJSON *parsed = 0; cJSON *parsed = 0;
enum SpecParseError retval = parse_spec_json(&config, &parsed); enum SpecParseError retval = parse_spec_json(&fixture->config, &parsed);
sput_fail_unless(retval == 0, "minimal spec.json, success"); sput_fail_unless(retval == 0, "minimal spec.json, success");
sput_fail_unless(parsed != 0, "minimal spec.json, parsed"); sput_fail_unless(parsed != 0, "minimal spec.json, parsed");
free(cwd); test_parser_teardown(fixture);
free(root_dir);
} }
static void test_parse_spec_json_invalid() { static void test_parse_spec_json_invalid() {
char *cwd = getcwd(0, 0); struct TestParserFixture *fixture = test_parser_setup("invalid_spec_json");
const char *segments[] = {cwd, "test", "specs"};
char *root_dir =
join_path_segments(sizeof(segments) / sizeof(char *), segments);
struct Config config = {
cwd,
root_dir,
"invalid_spec_json",
};
cJSON *parsed = 0; cJSON *parsed = 0;
enum SpecParseError retval = parse_spec_json(&config, &parsed); enum SpecParseError retval = parse_spec_json(&fixture->config, &parsed);
sput_fail_unless( sput_fail_unless(
retval == SPE_INVALID_SYNTAX, "invalid spec.json, INVALID_SYNTAX" retval == SPE_INVALID_SYNTAX, "invalid spec.json, INVALID_SYNTAX"
); );
sput_fail_unless(parsed == 0, "invalid spec.json, not parsed"); sput_fail_unless(parsed == 0, "invalid spec.json, not parsed");
free(cwd); test_parser_teardown(fixture);
free(root_dir);
} }
#endif /* _BOOTSTRAP_TEST_PARSER */ #endif /* _BOOTSTRAP_TEST_PARSER */