Add consistent error output.

pull/9/head
Joshua Potter 2023-11-26 06:30:44 -07:00
parent df65c8bcac
commit 80b1f4ed49
9 changed files with 161 additions and 59 deletions

View File

@ -2,10 +2,6 @@
CLI utility for defining custom project initialization scripts. CLI utility for defining custom project initialization scripts.
TODO:
- [ ] Add evaluator tests.
- [ ] Color output to console.
## Overview ## Overview
`bootstrap` is a tool for quickly defining your own init-like scripts. If you `bootstrap` is a tool for quickly defining your own init-like scripts. If you

View File

@ -95,7 +95,7 @@ Take the `__VA_ARGS__` list and append a list of decreasing numbers
__VA_ARGS__, \ __VA_ARGS__, \
0x1F, 0x1E, 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, \ 0x1F, 0x1E, 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, \
0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, \ 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, \
0x0E, 0x0F, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, \ 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, \
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00) 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00)
#define ALEN0( \ #define ALEN0( \
@ -121,13 +121,20 @@ It is the responsibility of the caller to free the @ref Error instance.
#define ERROR_NEW(code, ...) \ #define ERROR_NEW(code, ...) \
ERROR_NEW0(code, ALEN(__VA_ARGS__), __VA_ARGS__) ERROR_NEW0(code, ALEN(__VA_ARGS__), __VA_ARGS__)
#define ERROR_NEW0(code, nargs, ...) \ #define ERROR_NEW0(code, nargs, ...) \
priv_error_new( \ priv_error_new((code), (nargs), (const char *[nargs]){__VA_ARGS__})
(code), (nargs + 1), (const char * [nargs + 1]){__VA_ARGS__, "\n"} \
)
// clang-format on // clang-format on
#define ANSI_BLACK(...) "\e[0;30m", __VA_ARGS__, "\e[0m"
#define ANSI_RED(...) "\e[0;31m", __VA_ARGS__, "\e[0m"
#define ANSI_GREEN(...) "\e[0;32m", __VA_ARGS__, "\e[0m"
#define ANSI_YELLOW(...) "\e[0;33m", __VA_ARGS__, "\e[0m"
#define ANSI_BLUE(...) "\e[0;34m", __VA_ARGS__, "\e[0m"
#define ANSI_PURPLE(...) "\e[0;35m", __VA_ARGS__, "\e[0m"
#define ANSI_CYAN(...) "\e[0;36m", __VA_ARGS__, "\e[0m"
#define ANSI_WHITE(...) "\e[0;37m", __VA_ARGS__, "\e[0m"
/** /**
@brief Deallocates a previously allocated @ref Error isntance. @brief Deallocates a previously allocated @ref Error isntance.

View File

@ -6,6 +6,7 @@
#define _BOOTSTRAP_VALIDATOR_H #define _BOOTSTRAP_VALIDATOR_H
#include "cJSON.h" #include "cJSON.h"
#include "config.h"
#include "dyn_array.h" #include "dyn_array.h"
#include "error.h" #include "error.h"
@ -52,6 +53,8 @@ struct Field {
/** /**
@brief Verify the `spec.json` file is formatted correctly. @brief Verify the `spec.json` file is formatted correctly.
@param config
A reference to the parameters describing the desired spec.
@param parsed @param parsed
A possible null pointer to the parsed `spec.json` file. If null, this method A possible null pointer to the parsed `spec.json` file. If null, this method
simply sets *fields to a null pointer. simply sets *fields to a null pointer.
@ -62,7 +65,9 @@ struct Field {
A null pointer if no error occurs. Otherwise an @ref Error pointer. A null pointer if no error occurs. Otherwise an @ref Error pointer.
*/ */
struct Error *validate_spec_json( struct Error *validate_spec_json(
const cJSON *const parsed, struct DynArray **fields const struct Config *const config,
const cJSON *const parsed,
struct DynArray **fields
); );
#endif /* _BOOTSTRAP_VALIDATOR_H */ #endif /* _BOOTSTRAP_VALIDATOR_H */

2
main.c
View File

@ -33,7 +33,7 @@ static int run(const char *root_dir, const char *target) {
} }
struct DynArray *prompts = 0; struct DynArray *prompts = 0;
if ((error = validate_spec_json(parsed, &prompts))) { if ((error = validate_spec_json(config, parsed, &prompts))) {
fprintf(stderr, "%s", error->message); fprintf(stderr, "%s", error->message);
goto cleanup_parsed; goto cleanup_parsed;
} }

View File

@ -17,11 +17,19 @@ struct Error *config_new(
assert(target); assert(target);
if (cwd == 0) { if (cwd == 0) {
return ERROR_NEW(ERROR_CONFIG_ENV_CWD_INVALID, "Could not retrieve $CWD."); return ERROR_NEW(
ERROR_CONFIG_ENV_CWD_INVALID,
ANSI_RED("ERROR"),
": Could not retrieve ",
ANSI_CYAN("CWD"),
"."
);
} }
if (root_dir == 0) { if (root_dir == 0) {
return ERROR_NEW( return ERROR_NEW(
ERROR_CONFIG_ENV_ROOT_DIR_INVALID, "No specified root directory." ERROR_CONFIG_ENV_ROOT_DIR_INVALID,
ANSI_RED("ERROR"),
": Could not find root directory."
); );
} }
@ -35,18 +43,30 @@ struct Error *config_new(
if (stat_res == -1) { if (stat_res == -1) {
if (errno == ENOENT) { if (errno == ENOENT) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_CONFIG_TARGET_NOT_FOUND, "Spec ", filepath, " not found." ERROR_CONFIG_TARGET_NOT_FOUND,
ANSI_RED("ERROR"),
": Could not find ",
ANSI_BLUE(target),
" spec."
); );
} else { } else {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_CONFIG_TARGET_INVALID, "Spec ", filepath, " is invalid." ERROR_CONFIG_TARGET_INVALID,
ANSI_RED("ERROR"),
": ",
ANSI_BLUE(target),
" is invalid."
); );
} }
goto cleanup; goto cleanup;
} }
if (!S_ISDIR(sb.st_mode)) { if (!S_ISDIR(sb.st_mode)) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_CONFIG_TARGET_NOT_DIR, "Spec ", filepath, " is not a directory." ERROR_CONFIG_TARGET_NOT_DIR,
ANSI_RED("ERROR"),
": ",
ANSI_CYAN(filepath),
" is not a directory."
); );
goto cleanup; goto cleanup;
} }

View File

@ -23,17 +23,20 @@ static struct Error *find_run_exec(const struct Config *const config) {
if (stat_res == -1 && errno == ENOENT) { if (stat_res == -1 && errno == ENOENT) {
return ERROR_NEW( return ERROR_NEW(
ERROR_EVALUATOR_RUNNER_NOT_FOUND, ERROR_EVALUATOR_RUNNER_NOT_FOUND,
"Could not find ", ANSI_RED("NOT_FOUND"),
config->target, ": Could not find ",
"/runner" ANSI_BLUE(config->target, "/runner"),
"."
); );
} }
if (!(sb.st_mode & S_IXUSR)) { if (!(sb.st_mode & S_IXUSR)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_EVALUATOR_RUNNER_NOT_EXEC, ERROR_EVALUATOR_RUNNER_NOT_EXEC,
config->target, ANSI_RED("ERROR"),
"/runner is not executable." ": ",
ANSI_BLUE(config->target, "/runner"),
" is not executable."
); );
} }
@ -42,20 +45,21 @@ static struct Error *find_run_exec(const struct Config *const config) {
static const char *prompt_field(struct Field *field) { static const char *prompt_field(struct Field *field) {
assert(field); assert(field);
printf("%s", field->prompt);
char *response = calloc(1, 1024);
switch (field->type) { switch (field->type) {
case FT_TEXT: case FT_TEXT:
printf("%s", field->prompt);
// TODO: Probably want this buffer size to be a bit more dynamic. // TODO: Probably want this buffer size to be a bit more dynamic.
char *input = calloc(1, 1024); if (fgets(response, 1024, stdin)) {
if (fgets(input, 1024, stdin)) { size_t len = strlen(response);
size_t len = strlen(input); if (len > 0 && response[len - 1] == '\n') {
if (len > 0 && input[len - 1] == '\n') { response[len - 1] = '\0';
input[len - 1] = '\0';
} }
return input; return response;
} else { } else {
free(input); free(response);
return 0; return 0;
} }
} }
@ -92,7 +96,9 @@ int evaluate_runner(
const char *response = prompt_field(field); const char *response = prompt_field(field);
if (!response) { if (!response) {
*error = ERROR_NEW( *error = ERROR_NEW(
ERROR_EVALUATOR_RESPONSE_INVALID, "Could not read in response." ERROR_EVALUATOR_RESPONSE_INVALID,
ANSI_RED("ERROR"),
": Could not read response."
); );
string_buf_free(env_buf); string_buf_free(env_buf);
return EXIT_FAILURE; return EXIT_FAILURE;

View File

@ -19,7 +19,11 @@ static struct Error *find_spec_json(
*handle = fopen(filepath, "r"); *handle = fopen(filepath, "r");
if (!*handle && errno != ENOENT) { if (!*handle && errno != ENOENT) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_PARSER_SPEC_JSON_INVALID, config->target, "/spec.json is invalid." ERROR_PARSER_SPEC_JSON_INVALID,
ANSI_RED("ERROR"),
": ",
ANSI_BLUE(config->target, "/spec.json"),
" is invalid."
); );
} }
@ -60,7 +64,10 @@ struct Error *parse_spec_json(
if (!*parsed) { if (!*parsed) {
return ERROR_NEW( return ERROR_NEW(
ERROR_PARSER_SPEC_JSON_INVALID_SYNTAX, ERROR_PARSER_SPEC_JSON_INVALID_SYNTAX,
"The spec.json file contains invalid JSON." ANSI_RED("ERROR"),
": ",
ANSI_BLUE(config->target, "/spec.json"),
" contains invalid JSON."
); );
} }

View File

@ -4,27 +4,44 @@
#include "string_utils.h" #include "string_utils.h"
static struct Error *read_field(const cJSON *const field, struct Field **out) { static struct Error *read_field(
const struct Config *const config,
const cJSON *const field,
struct Field **out
) {
if (!cJSON_IsObject(field)) { if (!cJSON_IsObject(field)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NOT_OBJECT, ERROR_VALIDATOR_FIELD_NOT_OBJECT,
"Field \"", ANSI_RED("ERROR"),
field->string, ": Field ",
"\" is not a JSON object." ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" is not a JSON object."
); );
} }
if (isdigit(field->string[0])) { if (isdigit(field->string[0])) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NAME_INVALID, ERROR_VALIDATOR_FIELD_NAME_INVALID,
"Field names may not begin with a digit." ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" may not begin with a digit."
); );
} else { } else {
for (const char *c = field->string; *c; ++c) { for (const char *c = field->string; *c; ++c) {
if (*c != '_' && !isalnum(*c)) { if (*c != '_' && !isalnum(*c)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NAME_INVALID, ERROR_VALIDATOR_FIELD_NAME_INVALID,
"Field names must consist of alphanumeric characters or underscores." ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" must consist of only alphanumeric characters and underscores."
); );
} }
} }
@ -38,9 +55,14 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
if (!cJSON_IsString(type)) { if (!cJSON_IsString(type)) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_TYPE_INVALID, ERROR_VALIDATOR_FIELD_TYPE_INVALID,
"Field \"", ANSI_RED("ERROR"),
field->string, ": Field ",
"\" has non-string \"type\"." ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" has non-string ",
ANSI_PURPLE("type"),
"."
); );
goto cleanup; goto cleanup;
} }
@ -50,9 +72,14 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
} else { } else {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN,
"Field \"", ANSI_RED("ERROR"),
field->string, ": Field ",
"\" has unknown \"type\"." ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" has unknown ",
ANSI_PURPLE("type"),
"."
); );
goto cleanup; goto cleanup;
} }
@ -63,9 +90,14 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
} else { } else {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_PROMPT_INVALID, ERROR_VALIDATOR_FIELD_PROMPT_INVALID,
"Field \"", ANSI_RED("ERROR"),
field->string, ": Field ",
"\" has non-string \"prompt\"." ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" has non-string ",
ANSI_PURPLE("prompt"),
"."
); );
goto cleanup; goto cleanup;
} }
@ -78,7 +110,9 @@ cleanup:
} }
struct Error *validate_spec_json( struct Error *validate_spec_json(
const cJSON *const parsed, struct DynArray **fields const struct Config *const config,
const cJSON *const parsed,
struct DynArray **fields
) { ) {
*fields = 0; *fields = 0;
@ -90,7 +124,10 @@ struct Error *validate_spec_json(
if (!cJSON_IsObject(parsed)) { if (!cJSON_IsObject(parsed)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT, ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT,
"Top-level JSON value in spec.json is not an object." ANSI_RED("ERROR"),
": Top-level JSON value in ",
ANSI_BLUE(config->target, "/spec.json"),
" is not an object."
); );
} }
@ -102,7 +139,7 @@ struct Error *validate_spec_json(
cJSON *child = parsed->child; cJSON *child = parsed->child;
while (child) { while (child) {
struct Field *field = 0; struct Field *field = 0;
error = read_field(child, &field); error = read_field(config, child, &field);
if (error) { if (error) {
goto cleanup; goto cleanup;
} }

View File

@ -1,14 +1,18 @@
#ifndef _BOOTSTRAP_TEST_VALIDATOR #ifndef _BOOTSTRAP_TEST_VALIDATOR
#define _BOOTSTRAP_TEST_VALIDATOR #define _BOOTSTRAP_TEST_VALIDATOR
#include <unistd.h>
#include "dyn_array.h" #include "dyn_array.h"
#include "sput.h" #include "sput.h"
#include "string_utils.h"
#include "validator.h" #include "validator.h"
struct TestValidatorFixture { struct TestValidatorFixture {
const char *json; const char *json;
struct DynArray *prompts; struct DynArray *prompts;
cJSON *parsed; cJSON *parsed;
struct Config config;
}; };
static struct TestValidatorFixture *test_validator_setup(const char *json) { static struct TestValidatorFixture *test_validator_setup(const char *json) {
@ -17,6 +21,15 @@ static struct TestValidatorFixture *test_validator_setup(const char *json) {
fixture->json = json; fixture->json = json;
fixture->prompts = 0; fixture->prompts = 0;
fixture->parsed = cJSON_Parse(json); fixture->parsed = cJSON_Parse(json);
char *cwd = getcwd(0, 0);
const char *segments[] = {cwd, "test", "specs"};
char *root_dir = join(sizeof(segments) / sizeof(char *), segments, '/');
fixture->config.cwd = cwd;
fixture->config.root_dir = root_dir;
fixture->config.target = "minimal_spec_json";
return fixture; return fixture;
} }
@ -24,13 +37,16 @@ static void test_validator_teardown(struct TestValidatorFixture *fixture) {
if (fixture->parsed) { if (fixture->parsed) {
cJSON_Delete(fixture->parsed); cJSON_Delete(fixture->parsed);
} }
free((void *)fixture->config.cwd);
free((void *)fixture->config.root_dir);
free(fixture); free(fixture);
} }
static void test_validator_toplevel_not_object() { static void test_validator_toplevel_not_object() {
struct TestValidatorFixture *fixture = test_validator_setup("[]"); struct TestValidatorFixture *fixture = test_validator_setup("[]");
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless( sput_fail_unless(
error->code == ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT, "top-level not object" error->code == ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT, "top-level not object"
); );
@ -43,7 +59,8 @@ static void test_validator_field_not_object() {
struct TestValidatorFixture *fixture = struct TestValidatorFixture *fixture =
test_validator_setup("{\"key\": \"$UNKNOWN\"}"); test_validator_setup("{\"key\": \"$UNKNOWN\"}");
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless( sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_NOT_OBJECT, "field not object" error->code == ERROR_VALIDATOR_FIELD_NOT_OBJECT, "field not object"
); );
@ -61,7 +78,8 @@ static void test_validator_field_name_leading_digit() {
"}" "}"
); );
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless( sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_NAME_INVALID, error->code == ERROR_VALIDATOR_FIELD_NAME_INVALID,
"field name leading digit" "field name leading digit"
@ -80,7 +98,8 @@ static void test_validator_field_name_non_alnum() {
"}" "}"
); );
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless( sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_NAME_INVALID, "field name non alnum" error->code == ERROR_VALIDATOR_FIELD_NAME_INVALID, "field name non alnum"
); );
@ -98,7 +117,8 @@ static void test_validator_field_type_invalid() {
"}" "}"
); );
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless( sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_TYPE_INVALID, "field type invalid" error->code == ERROR_VALIDATOR_FIELD_TYPE_INVALID, "field type invalid"
); );
@ -116,7 +136,8 @@ static void test_validator_field_type_unknown() {
"}" "}"
); );
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless( sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, "field type unknown" error->code == ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, "field type unknown"
); );
@ -135,7 +156,8 @@ static void test_validator_valid_type_ci() {
"}" "}"
); );
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(error == 0, "valid"); sput_fail_unless(error == 0, "valid");
test_validator_teardown(fixture); test_validator_teardown(fixture);
@ -151,7 +173,8 @@ static void test_validator_field_prompt_invalid() {
"}" "}"
); );
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless( sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_PROMPT_INVALID, "field prompt invalid" error->code == ERROR_VALIDATOR_FIELD_PROMPT_INVALID, "field prompt invalid"
); );
@ -170,7 +193,8 @@ static void test_validator_valid() {
"}" "}"
); );
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts); struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(error == 0, "valid"); sput_fail_unless(error == 0, "valid");
test_validator_teardown(fixture); test_validator_teardown(fixture);