diff --git a/.github/workflows/cmake.yml b/.github/workflows/main.yml similarity index 97% rename from .github/workflows/cmake.yml rename to .github/workflows/main.yml index 292de68..33d0c82 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: CMake +name: CI on: push: diff --git a/README.md b/README.md index 039545c..4e9191b 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,14 @@ Types of options ---------------- * Boolean values are options that do not take a value, and are - either set or unset, for example `-v` or `--verbose`. Booleans - are shorthand for switches that are assigned a value of `1` when - present. + either set or unset, for example `-v` or `--verbose`. If a + boolean has a long name (eg `--verbose`) then it implicitly has + a negation prefixed by `no-` (in this case, `--no-verbose`). + The given value will be set to `1` when the boolean is given on + the command line and `0` when its negation is given. +* Accumulators are options that can be provided multiple times to + increase its value. For example, `-v` will set verbosity to `1`, + but `-vvv` will set verbosity to `3`. * Switches are options that do not take a value on the command line, for example `--long` or `--short`. When a switch is present on the command line, a variable will be set to a predetermined value. @@ -28,8 +33,8 @@ Types of options * Literal separators are bare double-dashes, `--` as a lone word, indicating that further words will be treated as arguments (even if they begin with a dash). -* Arguments are options that are bare words, not prefixed with - a single or double dash, for example `filename.txt`. +* Arguments are bare words, not prefixed with a single or double dash, + for example `filename.txt`. * Argument lists are the remainder of arguments, not prefixed with dashes. For example, an array: `file1.txt`, `file2.txt`, ... @@ -41,6 +46,7 @@ Options should be specified as an array of `adopt_spec` elements, elements, terminated with an `adopt_spec` initialized to zeroes. ```c +int debug = 0; int verbose = 0; int volume = 1; char *channel = "default"; @@ -48,8 +54,13 @@ char *filename1 = NULL; char *filename2 = NULL; adopt_spec opt_specs[] = { - /* `verbose`, above, will be set to `1` when specified. */ - { ADOPT_BOOL, "verbose", 'v', &verbose }, + /* `verbose`, above, will increment each time `-v` is specified. */ + { ADOPT_ACCUMULATOR, "verbose", 'v', &verbose }, + + /* `debug`, above, will be set to `1` when `--debug` is specified, + * or to `0` if `--no-debug` is specified. + */ + { ADOPT_BOOL, "debug", 'd', &debug }, /* `volume` will be set to `0` when `--quiet` is specified, and * set to `2` when `--loud` is specified. if neither is specified, @@ -99,7 +110,7 @@ int main(int argc, const char **argv) const char *value; const char *file; - if (adopt_parse(&result, opt_specs, argv + 1, argc - 1) < 0) { + if (adopt_parse(&result, opt_specs, argv + 1, argc - 1, ADOPT_PARSE_DEFAULT) < 0) { adopt_status_fprint(stderr, &opt); adopt_usage_fprint(stderr, argv[0], opt_specs); return 129; diff --git a/adopt.c b/adopt.c index 0ef811c..1fbe3be 100644 --- a/adopt.c +++ b/adopt.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "adopt.h" @@ -26,57 +27,89 @@ # define INLINE(type) static inline type #endif -#define spec_is_named_type(x) \ +#ifdef _MSC_VER +# define alloca _alloca +#endif + +#define spec_is_option_type(x) \ ((x)->type == ADOPT_TYPE_BOOL || \ (x)->type == ADOPT_TYPE_SWITCH || \ (x)->type == ADOPT_TYPE_VALUE) -INLINE(const adopt_spec *) spec_byname( - adopt_parser *parser, - const char *name, - size_t namelen, - int *is_converse) +INLINE(const adopt_spec *) spec_for_long( + int *is_negated, + int *has_value, + const char **value, + const adopt_parser *parser, + const char *arg) { const adopt_spec *spec; + char *eql; + size_t eql_pos; - *is_converse = 0; + eql = strchr(arg, '='); + eql_pos = (eql = strchr(arg, '=')) ? (size_t)(eql - arg) : strlen(arg); for (spec = parser->specs; spec->type; ++spec) { - if (spec->type == ADOPT_TYPE_LITERAL && namelen == 0) + /* Handle -- (everything after this is literal) */ + if (spec->type == ADOPT_TYPE_LITERAL && arg[0] == '\0') return spec; - /* Handle "no-" prefix for boolean types */ + /* Handle --no-option arguments for bool types */ if (spec->type == ADOPT_TYPE_BOOL && - strlen(spec->name) + 3 == namelen && - strncmp(name, "no-", 3) == 0 && - strncmp(name + 3, spec->name, namelen) == 0) { - *is_converse = 1; + strncmp(arg, "no-", 3) == 0 && + strcmp(arg + 3, spec->name) == 0) { + *is_negated = 1; return spec; } - if (spec_is_named_type(spec) && + /* Handle the typical --option arguments */ + if (spec_is_option_type(spec) && spec->name && - strlen(spec->name) == namelen && - strncmp(name, spec->name, namelen) == 0) + strcmp(arg, spec->name) == 0) + return spec; + + /* Handle --option=value arguments */ + if (spec->type == ADOPT_TYPE_VALUE && + eql && + strncmp(arg, spec->name, eql_pos) == 0 && + spec->name[eql_pos] == '\0') { + *has_value = 1; + *value = arg[eql_pos + 1] ? &arg[eql_pos + 1] : NULL; return spec; + } } return NULL; } -INLINE(const adopt_spec *) spec_byalias(adopt_parser *parser, char alias) +INLINE(const adopt_spec *) spec_for_short( + const char **value, + const adopt_parser *parser, + const char *arg) { const adopt_spec *spec; for (spec = parser->specs; spec->type; ++spec) { - if (spec_is_named_type(spec) && alias == spec->alias) + /* Handle -svalue short options with a value */ + if (spec->type == ADOPT_TYPE_VALUE && + arg[0] == spec->alias && + arg[1] != '\0') { + *value = &arg[1]; + return spec; + } + + /* Handle typical -s short options */ + if (arg[0] == spec->alias) { + *value = NULL; return spec; + } } return NULL; } -INLINE(const adopt_spec *) spec_nextarg(adopt_parser *parser) +INLINE(const adopt_spec *) spec_for_arg(adopt_parser *parser) { const adopt_spec *spec; size_t args = 0; @@ -127,15 +160,13 @@ INLINE(void) consume_choices(const adopt_spec *spec, adopt_parser *parser) static adopt_status_t parse_long(adopt_opt *opt, adopt_parser *parser) { const adopt_spec *spec; - char *arg = parser->args[parser->idx++], *name = arg + 2, *eql; - int converse = 0; - size_t namelen; - - namelen = (eql = strrchr(arg, '=')) ? (size_t)(eql - name) : strlen(name); + char *arg = parser->args[parser->idx++]; + const char *value = NULL; + int is_negated = 0, has_value = 0; opt->arg = arg; - if ((spec = spec_byname(parser, name, namelen, &converse)) == NULL) { + if ((spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2])) == NULL) { opt->spec = NULL; opt->status = ADOPT_STATUS_UNKNOWN_OPTION; goto done; @@ -147,16 +178,22 @@ static adopt_status_t parse_long(adopt_opt *opt, adopt_parser *parser) if (spec->type == ADOPT_TYPE_LITERAL) parser->in_literal = 1; + /* --bool or --no-bool */ else if (spec->type == ADOPT_TYPE_BOOL && spec->value) - *((int *)spec->value) = !converse; + *((int *)spec->value) = !is_negated; + + /* --accumulate */ + else if (spec->type == ADOPT_TYPE_ACCUMULATOR && spec->value) + *((int *)spec->value) += spec->switch_value ? spec->switch_value : 1; + /* --switch */ else if (spec->type == ADOPT_TYPE_SWITCH && spec->value) *((int *)spec->value) = spec->switch_value; /* Parse values as "--foo=bar" or "--foo bar" */ else if (spec->type == ADOPT_TYPE_VALUE) { - if (eql && *(eql+1)) - opt->value = eql + 1; + if (has_value) + opt->value = (char *)value; else if ((parser->idx + 1) <= parser->args_len) opt->value = parser->args[parser->idx++]; @@ -181,11 +218,12 @@ static adopt_status_t parse_long(adopt_opt *opt, adopt_parser *parser) static adopt_status_t parse_short(adopt_opt *opt, adopt_parser *parser) { const adopt_spec *spec; - char *arg = parser->args[parser->idx++], alias = *(arg + 1); + char *arg = parser->args[parser->idx++]; + const char *value; opt->arg = arg; - if ((spec = spec_byalias(parser, alias)) == NULL) { + if ((spec = spec_for_short(&value, parser, &arg[1 + parser->in_short])) == NULL) { opt->spec = NULL; opt->status = ADOPT_STATUS_UNKNOWN_OPTION; goto done; @@ -196,13 +234,16 @@ static adopt_status_t parse_short(adopt_opt *opt, adopt_parser *parser) if (spec->type == ADOPT_TYPE_BOOL && spec->value) *((int *)spec->value) = 1; - if (spec->type == ADOPT_TYPE_SWITCH && spec->value) + else if (spec->type == ADOPT_TYPE_ACCUMULATOR && spec->value) + *((int *)spec->value) += spec->switch_value ? spec->switch_value : 1; + + else if (spec->type == ADOPT_TYPE_SWITCH && spec->value) *((int *)spec->value) = spec->switch_value; /* Parse values as "-ifoo" or "-i foo" */ - if (spec->type == ADOPT_TYPE_VALUE) { - if (strlen(arg) > 2) - opt->value = arg + 2; + else if (spec->type == ADOPT_TYPE_VALUE) { + if (value) + opt->value = (char *)value; else if ((parser->idx + 1) <= parser->args_len) opt->value = parser->args[parser->idx++]; @@ -210,6 +251,18 @@ static adopt_status_t parse_short(adopt_opt *opt, adopt_parser *parser) *((char **)spec->value) = opt->value; } + /* + * Handle compressed short arguments, like "-fbcd"; see if there's + * another character after the one we processed. If not, advance + * the parser index. + */ + if (spec->type != ADOPT_TYPE_VALUE && arg[2 + parser->in_short] != '\0') { + parser->in_short++; + parser->idx--; + } else { + parser->in_short = 0; + } + /* Required argument was not provided */ if (spec->type == ADOPT_TYPE_VALUE && !opt->value) opt->status = ADOPT_STATUS_MISSING_VALUE; @@ -224,7 +277,7 @@ static adopt_status_t parse_short(adopt_opt *opt, adopt_parser *parser) static adopt_status_t parse_arg(adopt_opt *opt, adopt_parser *parser) { - const adopt_spec *spec = spec_nextarg(parser); + const adopt_spec *spec = spec_for_arg(parser); opt->spec = spec; opt->arg = parser->args[parser->idx]; @@ -236,7 +289,10 @@ static adopt_status_t parse_arg(adopt_opt *opt, adopt_parser *parser) if (spec->value) *((char ***)spec->value) = &parser->args[parser->idx]; - /* Args consume all the remaining arguments. */ + /* + * We have started a list of arguments; the remainder of + * given arguments need not be examined. + */ parser->in_args = (parser->args_len - parser->idx); parser->idx = parser->args_len; opt->args_len = parser->in_args; @@ -252,11 +308,32 @@ static adopt_status_t parse_arg(adopt_opt *opt, adopt_parser *parser) return opt->status; } +static int support_gnu_style(unsigned int flags) +{ + if ((flags & ADOPT_PARSE_FORCE_GNU) != 0) + return 1; + + if ((flags & ADOPT_PARSE_GNU) == 0) + return 0; + + /* TODO: Windows */ +#if defined(_WIN32) && defined(UNICODE) + if (_wgetenv(L"POSIXLY_CORRECT") != NULL) + return 0; +#else + if (getenv("POSIXLY_CORRECT") != NULL) + return 0; +#endif + + return 1; +} + void adopt_parser_init( adopt_parser *parser, const adopt_spec specs[], char **args, - size_t args_len) + size_t args_len, + unsigned int flags) { assert(parser); @@ -265,6 +342,112 @@ void adopt_parser_init( parser->specs = specs; parser->args = args; parser->args_len = args_len; + parser->flags = flags; + + parser->needs_sort = support_gnu_style(flags); +} + +INLINE(const adopt_spec *) spec_for_sort( + int *needs_value, + const adopt_parser *parser, + const char *arg) +{ + int is_negated, has_value = 0; + const char *value; + const adopt_spec *spec = NULL; + size_t idx = 0; + + *needs_value = 0; + + if (strncmp(arg, "--", 2) == 0) { + spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2]); + *needs_value = !has_value; + } + + else if (strncmp(arg, "-", 1) == 0) { + spec = spec_for_short(&value, parser, &arg[1]); + + /* + * Advance through compressed short arguments to see if + * the last one has a value, eg "-xvffilename". + */ + while (spec && !value && arg[1 + ++idx] != '\0') + spec = spec_for_short(&value, parser, &arg[1 + idx]); + + *needs_value = (value == NULL); + } + + return spec; +} + +/* + * Some parsers allow for handling arguments like "file1 --help file2"; + * this is done by re-sorting the arguments in-place; emulate that. + */ +static int sort_gnu_style(adopt_parser *parser) +{ + size_t i, j, insert_idx = parser->idx, offset; + const adopt_spec *spec; + char *option, *value; + int needs_value, changed = 0; + + parser->needs_sort = 0; + + for (i = parser->idx; i < parser->args_len; i++) { + spec = spec_for_sort(&needs_value, parser, parser->args[i]); + + /* Not a "-" or "--" prefixed option. No change. */ + if (!spec) + continue; + + /* A "--" alone means remaining args are literal. */ + if (spec->type == ADOPT_TYPE_LITERAL) + break; + + option = parser->args[i]; + + /* + * If the argument is a value type and doesn't already + * have a value (eg "--foo=bar" or "-fbar") then we need + * to copy the next argument as its value. + */ + if (spec->type == ADOPT_TYPE_VALUE && needs_value) { + /* + * A required value is not provided; set parser + * index to this value so that we fail on it. + */ + if (i + 1 >= parser->args_len) { + parser->idx = i; + return 1; + } + + value = parser->args[i + 1]; + offset = 1; + } else { + value = NULL; + offset = 0; + } + + /* Caller error if args[0] is an option. */ + if (i == 0) + return 0; + + /* Shift args up one (or two) and insert the option */ + for (j = i; j > insert_idx; j--) + parser->args[j + offset] = parser->args[j - 1]; + + parser->args[insert_idx] = option; + + if (value) + parser->args[insert_idx + 1] = value; + + insert_idx += (1 + offset); + i += offset; + + changed = 1; + } + + return changed; } adopt_status_t adopt_parser_next(adopt_opt *opt, adopt_parser *parser) @@ -278,19 +461,28 @@ adopt_status_t adopt_parser_next(adopt_opt *opt, adopt_parser *parser) return ADOPT_STATUS_DONE; } - /* Handle arguments in long form, those beginning with "--" */ + /* Handle options in long form, those beginning with "--" */ if (strncmp(parser->args[parser->idx], "--", 2) == 0 && - !parser->in_literal) + !parser->in_short && + !parser->in_literal) return parse_long(opt, parser); - /* Handle arguments in short form, those beginning with "-" */ - else if (strncmp(parser->args[parser->idx], "-", 1) == 0 && - !parser->in_literal) + /* Handle options in short form, those beginning with "-" */ + else if (parser->in_short || + (strncmp(parser->args[parser->idx], "-", 1) == 0 && + !parser->in_literal)) return parse_short(opt, parser); - /* Handle "free" arguments, those without a dash */ - else - return parse_arg(opt, parser); + /* + * We've reached the first "bare" argument. In POSIX mode, all + * remaining items on the command line are arguments. In GNU + * mode, there may be long or short options after this. Sort any + * options up to this position then re-parse the current position. + */ + if (parser->needs_sort && sort_gnu_style(parser)) + return adopt_parser_next(opt, parser); + + return parse_arg(opt, parser); } INLINE(int) spec_included(const adopt_spec **specs, const adopt_spec *spec) @@ -354,13 +546,14 @@ adopt_status_t adopt_parse( adopt_opt *opt, const adopt_spec specs[], char **args, - size_t args_len) + size_t args_len, + unsigned int flags) { adopt_parser parser; const adopt_spec **given_specs; size_t given_idx = 0; - adopt_parser_init(&parser, specs, args, args_len); + adopt_parser_init(&parser, specs, args, args_len, flags); given_specs = alloca(sizeof(const adopt_spec *) * (args_len + 1)); diff --git a/adopt.h b/adopt.h index ecf5db8..a6ea533 100644 --- a/adopt.h +++ b/adopt.h @@ -19,31 +19,63 @@ typedef enum { ADOPT_TYPE_NONE = 0, /** - * An argument that, when specified, sets a given value to true. - * This is useful for arguments like "--debug". A converse - * argument (beginning with "no-") is implicitly specified; for + * An option that, when specified, sets a given value to true. + * This is useful for options like "--debug". A negation + * option (beginning with "no-") is implicitly specified; for * example "--no-debug". The `value` pointer in the returned * option will be set to `1` when this is specified, and set to - * `0` when the converse "no-" argument is specified. + * `0` when the negation "no-" option is specified. */ ADOPT_TYPE_BOOL, /** - * An argument that, when specified, sets the given `value_ptr` - * to the given `value`. + * An option that, when specified, sets the given `value` pointer + * to the specified `switch_value`. This is useful for booleans + * where you do not want the implicit negation that comes with an + * `ADOPT_TYPE_BOOL`, or for switches that multiplex a value, like + * setting a mode. For example, `--read` may set the `value` to + * `MODE_READ` and `--write` may set the `value` to `MODE_WRITE`. */ ADOPT_TYPE_SWITCH, - /** An argument that has a value ("-nvalue" or "--name value") */ + /** + * An option that, when specified, increments the given + * `value` by the given `switch_value`. This can be specified + * multiple times to continue to increment the `value`. + * (For example, "-vvv" to set verbosity to 3.) + */ + ADOPT_TYPE_ACCUMULATOR, + + /** + * An option that takes a value, for example `-n value`, + * `-nvalue`, `--name value` or `--name=value`. + */ ADOPT_TYPE_VALUE, - /** The literal arguments follow specifier, bare "--" */ + /** + * A bare "--" that indicates that arguments following this are + * literal. This allows callers to specify things that might + * otherwise look like options, for example to operate on a file + * named "-rf" then you can invoke "program -- -rf" to treat + * "-rf" as an argument not an option. + */ ADOPT_TYPE_LITERAL, - /** A single "free" argument ("path") */ + /** + * A single argument, not an option. When options are exhausted, + * arguments will be matches in the order that they're specified + * in the spec list. For example, if two `ADOPT_TYPE_ARGS` are + * specified, `input_file` and `output_file`, then the first bare + * argument on the command line will be `input_file` and the + * second will be `output_file`. + */ ADOPT_TYPE_ARG, - /** Unmatched arguments, a collection of "free" arguments ("paths...") */ + /** + * A collection of arguments. This is useful when you want to take + * a list of arguments, for example, multiple paths. When specified, + * the value will be set to the first argument in the list. + */ ADOPT_TYPE_ARGS, } adopt_type_t; @@ -82,6 +114,25 @@ typedef enum { ADOPT_USAGE_SHOW_LONG = (1u << 5), } adopt_usage_t; +typedef enum { + /** Default parsing behavior. */ + ADOPT_PARSE_DEFAULT = 0, + + /** + * Parse with GNU `getopt_long` style behavior, where options can + * be intermixed with arguments at any position (for example, + * "file1 --help file2".) Like `getopt_long`, this can mutate the + * arguments given. + */ + ADOPT_PARSE_GNU = (1u << 0), + + /** + * Force GNU `getopt_long` style behavior; the `POSIXLY_CORRECT` + * environment variable is ignored. + */ + ADOPT_PARSE_FORCE_GNU = (1u << 1), +} adopt_flag_t; + /** Specification for an available option. */ typedef struct adopt_spec { /** Type of option expected. */ @@ -98,8 +149,13 @@ typedef struct adopt_spec { * to an `int` that will be set to `1` if the option is specified. * * If this spec is of type `ADOPT_TYPE_SWITCH`, this is a pointer - * to an `int` that will be set to the opt's `value` (below) when - * this option is specified. + * to an `int` that will be set to the opt's `switch_value` (below) + * when this option is specified. + * + * If this spec is of type `ADOPT_TYPE_ACCUMULATOR`, this is a + * pointer to an `int` that will be incremented by the opt's + * `switch_value` (below). If no `switch_value` is provided then + * the value will be incremented by 1. * * If this spec is of type `ADOPT_TYPE_VALUE`, * `ADOPT_TYPE_VALUE_OPTIONAL`, or `ADOPT_TYPE_ARG`, this is @@ -114,8 +170,10 @@ typedef struct adopt_spec { /** * If this spec is of type `ADOPT_TYPE_SWITCH`, this is the value - * to set in the option's `value_ptr` pointer when it is specified. - * This is ignored for other opt types. + * to set in the option's `value` pointer when it is specified. If + * this spec is of type `ADOPT_TYPE_ACCUMULATOR`, this is the value + * to increment in the option's `value` pointer when it is + * specified. This is ignored for other opt types. */ int switch_value; @@ -203,14 +261,38 @@ typedef struct adopt_parser { const adopt_spec *specs; char **args; size_t args_len; + unsigned int flags; + /* Parser state */ size_t idx; size_t arg_idx; size_t in_args; - int in_literal : 1, - in_short : 1; + size_t in_short; + int needs_sort : 1, + in_literal : 1; } adopt_parser; +/** + * Parses all the command-line arguments and updates all the options using + * the pointers provided. Parsing stops on any invalid argument and + * information about the failure will be provided in the opt argument. + * + * This is the simplest way to parse options; it handles the initialization + * (`parser_init`) and looping (`parser_next`). + * + * @param opt The The `adopt_opt` information that failed parsing + * @param specs A NULL-terminated array of `adopt_spec`s that can be parsed + * @param args The arguments that will be parsed + * @param args_len The length of arguments to be parsed + * @param flags The `adopt_flag_t flags for parsing + */ +adopt_status_t adopt_parse( + adopt_opt *opt, + const adopt_spec specs[], + char **args, + size_t args_len, + unsigned int flags); + /** * Initializes a parser that parses the given arguments according to the * given specifications. @@ -219,12 +301,14 @@ typedef struct adopt_parser { * @param specs A NULL-terminated array of `adopt_spec`s that can be parsed * @param args The arguments that will be parsed * @param args_len The length of arguments to be parsed + * @param flags The `adopt_flag_t flags for parsing */ void adopt_parser_init( adopt_parser *parser, const adopt_spec specs[], char **args, - size_t args_len); + size_t args_len, + unsigned int flags); /** * Parses the next command-line argument and places the information about @@ -240,22 +324,6 @@ adopt_status_t adopt_parser_next( adopt_opt *opt, adopt_parser *parser); -/** - * Parses all the command-line arguments and updates all the options using - * the pointers provided. Parsing stops on any invalid argument and - * information about the failure will be provided in the opt argument. - * - * @param opt The The `adopt_opt` information that failed parsing - * @param specs A NULL-terminated array of `adopt_spec`s that can be parsed - * @param args The arguments that will be parsed - * @param args_len The length of arguments to be parsed - */ -adopt_status_t adopt_parse( - adopt_opt *opt, - const adopt_spec specs[], - char **args, - size_t args_len); - /** * Prints the status after parsing the most recent argument. This is * useful for printing an error message when an unknown argument was diff --git a/examples/loop.c b/examples/loop.c index 79666db..027642c 100644 --- a/examples/loop.c +++ b/examples/loop.c @@ -7,6 +7,7 @@ */ #include +#include #include "adopt.h" @@ -55,8 +56,14 @@ int main(int argc, char **argv) adopt_parser parser; adopt_opt opt; size_t i; + unsigned int flags = ADOPT_PARSE_DEFAULT; - adopt_parser_init(&parser, opt_specs, argv + 1, argc - 1); + if (getenv("ADOPT_PARSE_GNU") != NULL) + flags |= ADOPT_PARSE_GNU; + if (getenv("ADOPT_PARSE_FORCE_GNU") != NULL) + flags |= ADOPT_PARSE_FORCE_GNU; + + adopt_parser_init(&parser, opt_specs, argv + 1, argc - 1, flags); while (adopt_parser_next(&opt, &parser)) { if (opt.status != ADOPT_STATUS_OK) { diff --git a/examples/parse.c b/examples/parse.c index 55c7eff..afc16b7 100644 --- a/examples/parse.c +++ b/examples/parse.c @@ -7,6 +7,7 @@ */ #include +#include #include "adopt.h" @@ -54,8 +55,14 @@ int main(int argc, char **argv) { adopt_opt result; size_t i; + unsigned int flags = ADOPT_PARSE_DEFAULT; - if (adopt_parse(&result, opt_specs, argv + 1, argc - 1) != 0) { + if (getenv("ADOPT_PARSE_GNU") != NULL) + flags |= ADOPT_PARSE_GNU; + if (getenv("ADOPT_PARSE_FORCE_GNU") != NULL) + flags |= ADOPT_PARSE_FORCE_GNU; + + if (adopt_parse(&result, opt_specs, argv + 1, argc - 1, flags) != 0) { adopt_status_fprint(stderr, argv[0], &result); adopt_usage_fprint(stderr, argv[0], opt_specs); return 129; diff --git a/rename.pl b/rename.pl index 044bf49..da6a6e8 100755 --- a/rename.pl +++ b/rename.pl @@ -115,7 +115,7 @@ sub translate { $contents =~ s/adopt_/${prefix}_/g; $contents =~ s/ADOPT_/${prefix_upper}_/g; - $contents =~ s/fprintf\(file, "([A-Z])/fprintf\(file, "\l$1/g if($lowercase_status); + $contents =~ s/fprintf\(file, "([A-Z])/fprintf\(file, "\l$1/g if($lowercase_status); if ($include) { $contents =~ s/^(#include "opt.h")$/#include "${include}"\n$1/mg; @@ -123,16 +123,14 @@ sub translate { if ($inline) { $contents =~ s/^INLINE/${inline}/mg; - $contents =~ s/\n#ifdef _MSC_VER\n.*\n#endif\n//sg; + $contents =~ s/\n#ifdef _MSC_VER\n.*?\n#endif\n//sg; } if ($tests) { $contents =~ s/test_adopt__/test_${tests_name}__/g; } - $contents =~ s/\n \*\/\n/\n *\n * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT.\n *\n * This file was produced by using the `rename.pl` script included with\n * adopt. The command-line specified was:\n *\n * $0 @ARGV\n *\/\n/s; - -# $contents =~ s/\n\n/\n\n\/*\n * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT.\n *\n * This file was produced by using the `rename.pl` script included with\n * adopt. The command-line specified was:\n *\n * $0 @ARGV\n *\/\n\n/s; + $contents =~ s/\n \*\/\n/\n *\n * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT.\n *\n * This file was produced by using the `rename.pl` script included with\n * adopt. The command-line specified was:\n *\n * $0 @ARGV\n *\/\n/s; open(OUT, '>' . $out) || die "$0: could not open ${out}: $!\n"; print OUT $contents; diff --git a/tests/adopt.c b/tests/adopt.c index 0fef25d..08be4d9 100644 --- a/tests/adopt.c +++ b/tests/adopt.c @@ -17,7 +17,7 @@ static void test_parse( adopt_opt opt; size_t i; - adopt_parser_init(&parser, specs, args, argslen); + adopt_parser_init(&parser, specs, args, argslen, ADOPT_PARSE_DEFAULT); for (i = 0; i < expectlen; ++i) { cl_assert(adopt_parser_next(&opt, &parser) > 0); @@ -44,7 +44,7 @@ static void test_returns_missing_value( adopt_opt opt; adopt_status_t status; - adopt_parser_init(&parser, specs, args, argslen); + adopt_parser_init(&parser, specs, args, argslen, ADOPT_PARSE_DEFAULT); status = adopt_parser_next(&opt, &parser); cl_assert_equal_i(ADOPT_STATUS_MISSING_VALUE, status); @@ -125,7 +125,7 @@ void test_adopt__returns_unknown_option(void) char *args[] = { "--unknown-long", "-u" }; - adopt_parser_init(&parser, specs, args, 2); + adopt_parser_init(&parser, specs, args, 2, ADOPT_PARSE_DEFAULT); status = adopt_parser_next(&opt, &parser); cl_assert_equal_i(ADOPT_STATUS_UNKNOWN_OPTION, status); @@ -341,14 +341,14 @@ void test_adopt__long_values4(void) { 0 } }; - char *args4[] = { "--foo=bar" }; + char *args4[] = { "--foo=--bar" }; adopt_expected expected4[] = { - { &specs[0], "bar" }, + { &specs[0], "--bar" }, }; /* Parse --foo=bar */ test_parse(specs, args4, 1, expected4, 1); - cl_assert_equal_s("bar", foo); + cl_assert_equal_s("--bar", foo); cl_assert_equal_s(NULL, bar); } @@ -602,7 +602,7 @@ void test_adopt__parse_oneshot(void) char *args[] = { "-f", "--bar" }; - cl_must_pass(adopt_parse(&result, specs, args, 2)); + cl_must_pass(adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i('f', foo); cl_assert_equal_i('b', bar); @@ -625,7 +625,7 @@ void test_adopt__parse_oneshot_unknown_option(void) char *args[] = { "-f", "--bar", "--asdf" }; - cl_assert_equal_i(ADOPT_STATUS_UNKNOWN_OPTION, adopt_parse(&result, specs, args, 3)); + cl_assert_equal_i(ADOPT_STATUS_UNKNOWN_OPTION, adopt_parse(&result, specs, args, 3, ADOPT_PARSE_DEFAULT)); } void test_adopt__parse_oneshot_missing_value(void) @@ -642,7 +642,7 @@ void test_adopt__parse_oneshot_missing_value(void) char *args[] = { "-f", "--bar" }; - cl_assert_equal_i(ADOPT_STATUS_MISSING_VALUE, adopt_parse(&result, specs, args, 2)); + cl_assert_equal_i(ADOPT_STATUS_MISSING_VALUE, adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); } void test_adopt__parse_arg(void) @@ -661,7 +661,7 @@ void test_adopt__parse_arg(void) char *args[] = { "-f", "bar", "baz" }; - cl_must_pass(adopt_parse(&result, specs, args, 3)); + cl_must_pass(adopt_parse(&result, specs, args, 3, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i('f', foo); cl_assert_equal_p(NULL, bar); @@ -675,8 +675,8 @@ void test_adopt__parse_arg(void) void test_adopt__parse_arg_mixed_with_switches(void) { - int foo = 0; - char *bar = NULL, *arg1 = NULL, *arg2 = NULL; + int foo = 0, bar = 0; + char *arg1 = NULL, *arg2 = NULL; adopt_opt result; adopt_spec specs[] = { @@ -689,10 +689,10 @@ void test_adopt__parse_arg_mixed_with_switches(void) char *args[] = { "-f", "bar", "baz", "--bar" }; - cl_must_pass(adopt_parse(&result, specs, args, 4)); + cl_must_pass(adopt_parse(&result, specs, args, 4, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i('f', foo); - cl_assert_equal_p('b', bar); + cl_assert_equal_i('b', bar); cl_assert_equal_s("bar", arg1); cl_assert_equal_p("baz", arg2); @@ -701,6 +701,82 @@ void test_adopt__parse_arg_mixed_with_switches(void) cl_assert_equal_p(NULL, result.value); } +void test_adopt__accumulator(void) +{ + int foo = 0; + adopt_opt result; + char *argz; + + char *args_zero[] = { "foo", "bar", "baz" }; + char *args_one[] = { "-f", "foo", "bar", "baz" }; + char *args_two[] = { "-f", "-f", "foo", "bar", "baz" }; + char *args_four[] = { "-f", "-f", "-f", "-f", "foo", "bar", "baz" }; + + adopt_spec specs[] = { + { ADOPT_TYPE_ACCUMULATOR, "foo", 'f', &foo, 0 }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_zero, 3, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(0, foo); + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_one, 4, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(1, foo); + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_two, 5, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(2, foo); + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_four, 7, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(4, foo); +} + +void test_adopt__accumulator_with_custom_incrementor(void) +{ + int foo = 0; + adopt_opt result; + char *argz; + + char *args_zero[] = { "foo", "bar", "baz" }; + char *args_one[] = { "-f", "foo", "bar", "baz" }; + char *args_two[] = { "-f", "-f", "foo", "bar", "baz" }; + char *args_four[] = { "-f", "-f", "-f", "-f", "foo", "bar", "baz" }; + + adopt_spec specs[] = { + { ADOPT_TYPE_ACCUMULATOR, "foo", 'f', &foo, 42 }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_zero, 3, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(0, foo); + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_one, 4, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(42, foo); + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_two, 5, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(84, foo); + + foo = 0; + cl_must_pass(adopt_parse(&result, specs, args_four, 7, ADOPT_PARSE_DEFAULT)); + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_i(168, foo); +} + void test_adopt__parse_arg_with_literal(void) { int foo = 0; @@ -718,7 +794,7 @@ void test_adopt__parse_arg_with_literal(void) char *args[] = { "-f", "--", "--bar" }; - cl_must_pass(adopt_parse(&result, specs, args, 3)); + cl_must_pass(adopt_parse(&result, specs, args, 3, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i('f', foo); cl_assert_equal_p(NULL, bar); @@ -745,7 +821,7 @@ void test_adopt__parse_args(void) char *args[] = { "-f", "--bar", "BRR", "one", "two", "three", "four" }; - cl_must_pass(adopt_parse(&result, specs, args, 7)); + cl_must_pass(adopt_parse(&result, specs, args, 7, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); cl_assert_equal_p(NULL, result.arg); @@ -777,7 +853,7 @@ void test_adopt__parse_args_with_literal(void) char *args[] = { "-f", "--", "--bar", "asdf", "--baz" }; - cl_must_pass(adopt_parse(&result, specs, args, 5)); + cl_must_pass(adopt_parse(&result, specs, args, 5, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); cl_assert_equal_p(NULL, result.arg); @@ -807,7 +883,7 @@ void test_adopt__parse_args_implies_literal(void) char *args[] = { "-f", "foo", "bar", "--bar" }; - cl_must_pass(adopt_parse(&result, specs, args, 4)); + cl_must_pass(adopt_parse(&result, specs, args, 4, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); cl_assert_equal_p(NULL, result.arg); @@ -822,6 +898,214 @@ void test_adopt__parse_args_implies_literal(void) cl_assert_equal_s("--bar", argz[2]); } +void test_adopt__parse_options_gnustyle(void) +{ + int foo = 0; + char *bar = NULL, **argz = NULL; + char **args; + adopt_opt result; + + adopt_spec specs[] = { + { ADOPT_TYPE_SWITCH, "foo", 'f', &foo, 'f' }, + { ADOPT_TYPE_VALUE, "bar", 0, &bar, 0 }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + /* allocate so that `adopt_parse` can reorder things */ + cl_assert(args = malloc(sizeof(char *) * 7)); + args[0] = "BRR"; + args[1] = "-f"; + args[2] = "one"; + args[3] = "two"; + args[4] = "--bar"; + args[5] = "three"; + args[6] = "four"; + + cl_must_pass(adopt_parse(&result, specs, args, 7, ADOPT_PARSE_GNU)); + + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_p(NULL, result.arg); + cl_assert_equal_p(NULL, result.value); + cl_assert_equal_i(4, result.args_len); + + cl_assert_equal_i('f', foo); + cl_assert_equal_s("three", bar); + cl_assert(argz); + cl_assert_equal_s("BRR", argz[0]); + cl_assert_equal_s("one", argz[1]); + cl_assert_equal_s("two", argz[2]); + cl_assert_equal_s("four", argz[3]); + + free(args); +} + +void test_adopt__parse_options_gnustyle_dangling_value(void) +{ + int foo = 0; + char *bar = NULL, **argz = NULL; + char **args; + adopt_opt result; + + adopt_spec specs[] = { + { ADOPT_TYPE_SWITCH, "foo", 'f', &foo, 'f' }, + { ADOPT_TYPE_VALUE, "bar", 0, &bar, 0 }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + /* allocate so that `adopt_parse` can reorder things */ + cl_assert(args = malloc(sizeof(char *) * 7)); + args[0] = "BRR"; + args[1] = "-f"; + args[2] = "one"; + args[3] = "two"; + args[4] = "three"; + args[5] = "four"; + args[6] = "--bar"; + + cl_must_pass(adopt_parse(&result, specs, args, 7, ADOPT_PARSE_GNU)); + + cl_assert_equal_i(ADOPT_STATUS_MISSING_VALUE, result.status); + cl_assert_equal_s("--bar", result.arg); + + free(args); +} + +void test_adopt__parse_options_gnustyle_with_compressed_shorts(void) +{ + int foo = 0, baz = 0; + char *bar = NULL, **argz = NULL; + char **args; + adopt_opt result; + + adopt_spec specs[] = { + { ADOPT_TYPE_SWITCH, "foo", 'f', &foo, 'f' }, + { ADOPT_TYPE_BOOL, "baz", 'z', &baz, 0 }, + { ADOPT_TYPE_VALUE, "bar", 'b', &bar, 'b' }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + /* allocate so that `adopt_parse` can reorder things */ + cl_assert(args = malloc(sizeof(char *) * 7)); + args[0] = "BRR"; + args[1] = "-fzb"; + args[2] = "bar"; + args[3] = "one"; + args[4] = "two"; + args[5] = "three"; + args[6] = "four"; + + cl_must_pass(adopt_parse(&result, specs, args, 7, ADOPT_PARSE_GNU)); + + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_p(NULL, result.arg); + cl_assert_equal_p(NULL, result.value); + cl_assert_equal_i(5, result.args_len); + + cl_assert_equal_i('f', foo); + cl_assert_equal_s("bar", bar); + cl_assert(argz); + cl_assert_equal_s("BRR", argz[0]); + cl_assert_equal_s("one", argz[1]); + cl_assert_equal_s("two", argz[2]); + cl_assert_equal_s("three", argz[3]); + cl_assert_equal_s("four", argz[4]); + + free(args); +} + +void test_adopt__compressed_shorts1(void) +{ + int foo = 0, baz = 0; + char *bar = NULL, **argz = NULL; + adopt_opt result; + + adopt_spec specs[] = { + { ADOPT_TYPE_SWITCH, "foo", 'f', &foo, 'f' }, + { ADOPT_TYPE_BOOL, "baz", 'z', &baz, 0 }, + { ADOPT_TYPE_VALUE, "bar", 'b', &bar, 'b' }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + char *args[] = { "-fzb", "asdf", "foobar" }; + + cl_must_pass(adopt_parse(&result, specs, args, 3, ADOPT_PARSE_DEFAULT)); + + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_p(NULL, result.arg); + cl_assert_equal_p(NULL, result.value); + cl_assert_equal_i(1, result.args_len); + + cl_assert_equal_i('f', foo); + cl_assert_equal_i(1, baz); + cl_assert_equal_s("asdf", bar); + cl_assert(argz); + cl_assert_equal_s("foobar", argz[0]); +} + +void test_adopt__compressed_shorts2(void) +{ + int foo = 0, baz = 0; + char *bar = NULL, **argz = NULL; + adopt_opt result; + + adopt_spec specs[] = { + { ADOPT_TYPE_SWITCH, "foo", 'f', &foo, 'f' }, + { ADOPT_TYPE_BOOL, "baz", 'z', &baz, 0 }, + { ADOPT_TYPE_VALUE, "bar", 'b', &bar, 'b' }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + char *args[] = { "-fzbasdf", "foobar" }; + + cl_must_pass(adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); + + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_p(NULL, result.arg); + cl_assert_equal_p(NULL, result.value); + cl_assert_equal_i(1, result.args_len); + + cl_assert_equal_i('f', foo); + cl_assert_equal_i(1, baz); + cl_assert_equal_s("asdf", bar); + cl_assert(argz); + cl_assert_equal_s("foobar", argz[0]); +} + +void test_adopt__compressed_shorts3(void) +{ + int foo = 0, baz = 0; + char *bar = NULL, **argz = NULL; + adopt_opt result; + + adopt_spec specs[] = { + { ADOPT_TYPE_SWITCH, "foo", 'f', &foo, 'f' }, + { ADOPT_TYPE_BOOL, "baz", 'z', &baz, 0 }, + { ADOPT_TYPE_VALUE, "bar", 'b', &bar, 'b' }, + { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0 }, + { 0 }, + }; + + char *args[] = { "-fbzasdf", "foobar" }; + + cl_must_pass(adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); + + cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); + cl_assert_equal_p(NULL, result.arg); + cl_assert_equal_p(NULL, result.value); + cl_assert_equal_i(1, result.args_len); + + cl_assert_equal_i('f', foo); + cl_assert_equal_i(0, baz); + cl_assert_equal_s("zasdf", bar); + cl_assert(argz); + cl_assert_equal_s("foobar", argz[0]); +} + void test_adopt__value_required(void) { int foo = 0; @@ -837,7 +1121,7 @@ void test_adopt__value_required(void) char *args[] = { "-f", "--bar" }; - cl_must_pass(adopt_parse(&result, specs, args, 2)); + cl_must_pass(adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_MISSING_VALUE, result.status); } @@ -857,7 +1141,7 @@ void test_adopt__required_choice_missing(void) char *args[] = { "foo", "bar" }; - cl_assert_equal_i(ADOPT_STATUS_MISSING_ARGUMENT, adopt_parse(&result, specs, args, 2)); + cl_assert_equal_i(ADOPT_STATUS_MISSING_ARGUMENT, adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_MISSING_ARGUMENT, result.status); cl_assert_equal_s("foo", result.spec->name); @@ -876,14 +1160,14 @@ void test_adopt__required_choice_specified(void) adopt_spec specs[] = { { ADOPT_TYPE_SWITCH, "foo", 'f', &foo, 'f', ADOPT_USAGE_REQUIRED }, { ADOPT_TYPE_VALUE, "bar", 0, &bar, 'b', ADOPT_USAGE_CHOICE }, - { ADOPT_TYPE_ARG, "baz", 0, &baz, 'z', ADOPT_USAGE_REQUIRED }, + { ADOPT_TYPE_ARG, "baz", 0, &baz, 0, ADOPT_USAGE_REQUIRED }, { ADOPT_TYPE_ARGS, "argz", 0, &argz, 0, 0 }, { 0 }, }; - char *args[] = { "--bar" }; + char *args[] = { "--bar", "b" }; - cl_assert_equal_i(ADOPT_STATUS_MISSING_ARGUMENT, adopt_parse(&result, specs, args, 2)); + cl_assert_equal_i(ADOPT_STATUS_MISSING_ARGUMENT, adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_MISSING_ARGUMENT, result.status); cl_assert_equal_s("baz", result.spec->name); @@ -910,7 +1194,7 @@ void test_adopt__choice_switch_or_arg_advances_arg(void) char *args[] = { "-z", "actually_final" }; - cl_assert_equal_i(ADOPT_STATUS_DONE, adopt_parse(&result, specs, args, 2)); + cl_assert_equal_i(ADOPT_STATUS_DONE, adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); cl_assert_equal_p(NULL, result.arg); @@ -938,7 +1222,7 @@ void test_adopt__stop(void) char *args[] = { "-f", "--help", "-z" }; - cl_assert_equal_i(ADOPT_STATUS_DONE, adopt_parse(&result, specs, args, 2)); + cl_assert_equal_i(ADOPT_STATUS_DONE, adopt_parse(&result, specs, args, 2, ADOPT_PARSE_DEFAULT)); cl_assert_equal_i(ADOPT_STATUS_DONE, result.status); cl_assert_equal_s("--help", result.arg); diff --git a/tests/clar.c b/tests/clar.c index f84d6cb..83d87b7 100644 --- a/tests/clar.c +++ b/tests/clar.c @@ -11,6 +11,7 @@ #include #include #include +#include /* required for sandboxing */ #include @@ -66,7 +67,7 @@ # define PRIxZ "Ix" # endif -# ifdef _MSC_VER +# if defined(_MSC_VER) || defined(__MINGW32__) typedef struct stat STAT_T; # else typedef struct _stat STAT_T; @@ -85,54 +86,90 @@ typedef struct stat STAT_T; #endif +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) + #include "clar.h" -__attribute__((unused)) static void fs_rm(const char *_source); -__attribute__((unused)) static void fs_copy(const char *_source, const char *dest); -__attribute__((unused)) +#ifdef CLAR_FIXTURE_PATH static const char * fixture_path(const char *base, const char *fixture_name); +#endif struct clar_error { - const char *test; - int test_number; - const char *suite; const char *file; - int line_number; + const char *function; + size_t line_number; const char *error_msg; char *description; struct clar_error *next; }; +struct clar_explicit { + size_t suite_idx; + const char *filter; + + struct clar_explicit *next; +}; + +struct clar_report { + const char *test; + int test_number; + const char *suite; + + enum cl_test_status status; + + struct clar_error *errors; + struct clar_error *last_error; + + struct clar_report *next; +}; + +struct clar_summary { + const char *filename; + FILE *fp; +}; + static struct { - int argc; - char **argv; + enum cl_test_status test_status; const char *active_test; const char *active_suite; - int suite_errors; + int total_skipped; int total_errors; int tests_ran; int suites_ran; + enum cl_output_format output_format; + int report_errors_only; int exit_on_error; - int report_suite_names; + int verbosity; - struct clar_error *errors; - struct clar_error *last_error; + int write_summary; + char *summary_filename; + struct clar_summary *summary; + + struct clar_explicit *explicit; + struct clar_explicit *last_explicit; + + struct clar_report *reports; + struct clar_report *last_report; void (*local_cleanup)(void *); void *local_cleanup_payload; jmp_buf trampoline; int trampoline_enabled; + + cl_trace_cb *pfn_trace_cb; + void *trace_payload; + } _clar; struct clar_func { @@ -152,8 +189,8 @@ struct clar_suite { /* From clar_print_*.c */ static void clar_print_init(int test_count, int suite_count, const char *suite_names); static void clar_print_shutdown(int test_count, int suite_count, int error_count); -static void clar_print_error(int num, const struct clar_error *error); -static void clar_print_ontest(const char *test_name, int test_number, int failed); +static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error); +static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed); static void clar_print_onsuite(const char *suite_name, int suite_index); static void clar_print_onabort(const char *msg, ...); @@ -161,67 +198,101 @@ static void clar_print_onabort(const char *msg, ...); static void clar_unsandbox(void); static int clar_sandbox(void); +/* From summary.h */ +static struct clar_summary *clar_summary_init(const char *filename); +static int clar_summary_shutdown(struct clar_summary *fp); + /* Load the declarations for the test suite */ #include "clar.suite" + +#define CL_TRACE(ev) \ + do { \ + if (_clar.pfn_trace_cb) \ + _clar.pfn_trace_cb(ev, \ + _clar.active_suite, \ + _clar.active_test, \ + _clar.trace_payload); \ + } while (0) + +void cl_trace_register(cl_trace_cb *cb, void *payload) +{ + _clar.pfn_trace_cb = cb; + _clar.trace_payload = payload; +} + + /* Core test functions */ static void -clar_report_errors(void) +clar_report_errors(struct clar_report *report) { + struct clar_error *error; int i = 1; - struct clar_error *error, *next; - - error = _clar.errors; - while (error != NULL) { - next = error->next; - clar_print_error(i++, error); - free(error->description); - free(error); - error = next; - } - _clar.errors = _clar.last_error = NULL; + for (error = report->errors; error; error = error->next) + clar_print_error(i++, _clar.last_report, error); +} + +static void +clar_report_all(void) +{ + struct clar_report *report; + struct clar_error *error; + int i = 1; + + for (report = _clar.reports; report; report = report->next) { + if (report->status != CL_TEST_FAILURE) + continue; + + for (error = report->errors; error; error = error->next) + clar_print_error(i++, report, error); + } } static void clar_run_test( + const struct clar_suite *suite, const struct clar_func *test, const struct clar_func *initialize, const struct clar_func *cleanup) { - int error_st = _clar.suite_errors; - _clar.trampoline_enabled = 1; + CL_TRACE(CL_TRACE__TEST__BEGIN); + if (setjmp(_clar.trampoline) == 0) { if (initialize->ptr != NULL) initialize->ptr(); + CL_TRACE(CL_TRACE__TEST__RUN_BEGIN); test->ptr(); + CL_TRACE(CL_TRACE__TEST__RUN_END); } _clar.trampoline_enabled = 0; + if (_clar.last_report->status == CL_TEST_NOTRUN) + _clar.last_report->status = CL_TEST_OK; + if (_clar.local_cleanup != NULL) _clar.local_cleanup(_clar.local_cleanup_payload); if (cleanup->ptr != NULL) cleanup->ptr(); + CL_TRACE(CL_TRACE__TEST__END); + _clar.tests_ran++; /* remove any local-set cleanup methods */ _clar.local_cleanup = NULL; _clar.local_cleanup_payload = NULL; - if (_clar.report_errors_only) - clar_report_errors(); - else - clar_print_ontest( - test->name, - _clar.tests_ran, - (_clar.suite_errors > error_st) - ); + if (_clar.report_errors_only) { + clar_report_errors(_clar.last_report); + } else { + clar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status); + } } static void @@ -229,6 +300,8 @@ clar_run_suite(const struct clar_suite *suite, const char *filter) { const struct clar_func *test = suite->tests; size_t i, matchlen; + struct clar_report *report; + int exact = 0; if (!suite->enabled) return; @@ -240,7 +313,8 @@ clar_run_suite(const struct clar_suite *suite, const char *filter) clar_print_onsuite(suite->name, ++_clar.suites_ran); _clar.active_suite = suite->name; - _clar.suite_errors = 0; + _clar.active_test = NULL; + CL_TRACE(CL_TRACE__SUITE_BEGIN); if (filter) { size_t suitelen = strlen(suite->name); @@ -252,6 +326,11 @@ clar_run_suite(const struct clar_suite *suite, const char *filter) while (*filter == ':') ++filter; matchlen = strlen(filter); + + if (matchlen && filter[matchlen - 1] == '$') { + exact = 1; + matchlen--; + } } } @@ -259,12 +338,33 @@ clar_run_suite(const struct clar_suite *suite, const char *filter) if (filter && strncmp(test[i].name, filter, matchlen)) continue; + if (exact && strlen(test[i].name) != matchlen) + continue; + _clar.active_test = test[i].name; - clar_run_test(&test[i], &suite->initialize, &suite->cleanup); + + report = calloc(1, sizeof(struct clar_report)); + report->suite = _clar.active_suite; + report->test = _clar.active_test; + report->test_number = _clar.tests_ran; + report->status = CL_TEST_NOTRUN; + + if (_clar.reports == NULL) + _clar.reports = report; + + if (_clar.last_report != NULL) + _clar.last_report->next = report; + + _clar.last_report = report; + + clar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup); if (_clar.exit_on_error && _clar.total_errors) return; } + + _clar.active_test = NULL; + CL_TRACE(CL_TRACE__SUITE_END); } static void @@ -272,12 +372,15 @@ clar_usage(const char *arg) { printf("Usage: %s [options]\n\n", arg); printf("Options:\n"); - printf(" -sname\tRun only the suite with `name` (can go to individual test name)\n"); - printf(" -iname\tInclude the suite with `name`\n"); - printf(" -xname\tExclude the suite with `name`\n"); - printf(" -q \tOnly report tests that had an error\n"); - printf(" -Q \tQuit as soon as a test fails\n"); - printf(" -l \tPrint suite names\n"); + printf(" -sname Run only the suite with `name` (can go to individual test name)\n"); + printf(" -iname Include the suite with `name`\n"); + printf(" -xname Exclude the suite with `name`\n"); + printf(" -v Increase verbosity (show suite names)\n"); + printf(" -q Only report tests that had an error\n"); + printf(" -Q Quit as soon as a test fails\n"); + printf(" -t Display results in tap format\n"); + printf(" -l Print suite names\n"); + printf(" -r[filename] Write summary file (to the optional filename)\n"); exit(-1); } @@ -286,11 +389,18 @@ clar_parse_args(int argc, char **argv) { int i; + /* Verify options before execute */ for (i = 1; i < argc; ++i) { char *argument = argv[i]; - if (argument[0] != '-') + if (argument[0] != '-' || argument[1] == '\0' + || strchr("sixvqQtlr", argument[1]) == NULL) { clar_usage(argv[0]); + } + } + + for (i = 1; i < argc; ++i) { + char *argument = argv[i]; switch (argument[1]) { case 's': @@ -313,13 +423,36 @@ clar_parse_args(int argc, char **argv) if (strncmp(argument, _clar_suites[j].name, cmplen) == 0) { int exact = (arglen >= suitelen); + /* Do we have a real suite prefix separated by a + * trailing '::' or just a matching substring? */ + if (arglen > suitelen && (argument[suitelen] != ':' + || argument[suitelen + 1] != ':')) + continue; + ++found; if (!exact) - _clar.report_suite_names = 1; + _clar.verbosity = MAX(_clar.verbosity, 1); switch (action) { - case 's': clar_run_suite(&_clar_suites[j], argument); break; + case 's': { + struct clar_explicit *explicit = + calloc(1, sizeof(struct clar_explicit)); + assert(explicit); + + explicit->suite_idx = j; + explicit->filter = argument; + + if (_clar.explicit == NULL) + _clar.explicit = explicit; + + if (_clar.last_explicit != NULL) + _clar.last_explicit->next = explicit; + + _clar_suites[j].enabled = 1; + _clar.last_explicit = explicit; + break; + } case 'i': _clar_suites[j].enabled = 1; break; case 'x': _clar_suites[j].enabled = 0; break; } @@ -344,6 +477,10 @@ clar_parse_args(int argc, char **argv) _clar.exit_on_error = 1; break; + case 't': + _clar.output_format = CL_OUTPUT_TAP; + break; + case 'l': { size_t j; printf("Test suites (use -s to run just one):\n"); @@ -353,8 +490,19 @@ clar_parse_args(int argc, char **argv) exit(0); } + case 'v': + _clar.verbosity++; + printf("VERBOSITY: _clar.verbosity: %d\n", _clar.verbosity); + break; + + case 'r': + _clar.write_summary = 1; + free(_clar.summary_filename); + _clar.summary_filename = strdup(*(argument + 2) ? (argument + 2) : "summary.xml"); + break; + default: - clar_usage(argv[0]); + assert(!"Unexpected commandline argument!"); } } } @@ -362,29 +510,42 @@ clar_parse_args(int argc, char **argv) void clar_test_init(int argc, char **argv) { + if (argc > 1) + clar_parse_args(argc, argv); + clar_print_init( (int)_clar_callback_count, (int)_clar_suite_count, "" ); + if ((_clar.summary_filename = getenv("CLAR_SUMMARY")) != NULL) { + _clar.write_summary = 1; + _clar.summary_filename = strdup(_clar.summary_filename); + } + + if (_clar.write_summary && + !(_clar.summary = clar_summary_init(_clar.summary_filename))) { + clar_print_onabort("Failed to open the summary file\n"); + exit(-1); + } + if (clar_sandbox() < 0) { clar_print_onabort("Failed to sandbox the test runner.\n"); exit(-1); } - - _clar.argc = argc; - _clar.argv = argv; } int -clar_test_run() +clar_test_run(void) { - if (_clar.argc > 1) - clar_parse_args(_clar.argc, _clar.argv); + size_t i; + struct clar_explicit *explicit; - if (!_clar.suites_ran) { - size_t i; + if (_clar.explicit) { + for (explicit = _clar.explicit; explicit; explicit = explicit->next) + clar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter); + } else { for (i = 0; i < _clar_suite_count; ++i) clar_run_suite(&_clar_suites[i], NULL); } @@ -393,8 +554,11 @@ clar_test_run() } void -clar_test_shutdown() +clar_test_shutdown(void) { + struct clar_explicit *explicit, *explicit_next; + struct clar_report *report, *report_next; + clar_print_shutdown( _clar.tests_ran, (int)_clar_suite_count, @@ -402,6 +566,23 @@ clar_test_shutdown() ); clar_unsandbox(); + + if (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) { + clar_print_onabort("Failed to write the summary file\n"); + exit(-1); + } + + for (explicit = _clar.explicit; explicit; explicit = explicit_next) { + explicit_next = explicit->next; + free(explicit); + } + + for (report = _clar.reports; report; report = report_next) { + report_next = report->next; + free(report); + } + + free(_clar.summary_filename); } int @@ -416,52 +597,64 @@ clar_test(int argc, char **argv) return errors; } +static void abort_test(void) +{ + if (!_clar.trampoline_enabled) { + clar_print_onabort( + "Fatal error: a cleanup method raised an exception."); + clar_report_errors(_clar.last_report); + exit(-1); + } + + CL_TRACE(CL_TRACE__TEST__LONGJMP); + longjmp(_clar.trampoline, -1); +} + +void clar__skip(void) +{ + _clar.last_report->status = CL_TEST_SKIP; + _clar.total_skipped++; + abort_test(); +} + void clar__fail( const char *file, - int line, + const char *function, + size_t line, const char *error_msg, const char *description, int should_abort) { struct clar_error *error = calloc(1, sizeof(struct clar_error)); - if (_clar.errors == NULL) - _clar.errors = error; + if (_clar.last_report->errors == NULL) + _clar.last_report->errors = error; - if (_clar.last_error != NULL) - _clar.last_error->next = error; + if (_clar.last_report->last_error != NULL) + _clar.last_report->last_error->next = error; - _clar.last_error = error; + _clar.last_report->last_error = error; - error->test = _clar.active_test; - error->test_number = _clar.tests_ran; - error->suite = _clar.active_suite; error->file = file; + error->function = function; error->line_number = line; error->error_msg = error_msg; if (description != NULL) error->description = strdup(description); - _clar.suite_errors++; _clar.total_errors++; + _clar.last_report->status = CL_TEST_FAILURE; - if (should_abort) { - if (!_clar.trampoline_enabled) { - clar_print_onabort( - "Fatal error: a cleanup method raised an exception."); - clar_report_errors(); - exit(-1); - } - - longjmp(_clar.trampoline, -1); - } + if (should_abort) + abort_test(); } void clar__assert( int condition, const char *file, - int line, + const char *function, + size_t line, const char *error_msg, const char *description, int should_abort) @@ -469,12 +662,13 @@ void clar__assert( if (condition) return; - clar__fail(file, line, error_msg, description, should_abort); + clar__fail(file, function, line, error_msg, description, should_abort); } void clar__assert_equal( const char *file, - int line, + const char *function, + size_t line, const char *err, int should_abort, const char *fmt, @@ -521,6 +715,41 @@ void clar__assert_equal( } } } + else if (!strcmp("%ls", fmt)) { + const wchar_t *wcs1 = va_arg(args, const wchar_t *); + const wchar_t *wcs2 = va_arg(args, const wchar_t *); + is_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2); + + if (!is_equal) { + if (wcs1 && wcs2) { + int pos; + for (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos) + /* find differing byte offset */; + p_snprintf(buf, sizeof(buf), "'%ls' != '%ls' (at byte %d)", + wcs1, wcs2, pos); + } else { + p_snprintf(buf, sizeof(buf), "'%ls' != '%ls'", wcs1, wcs2); + } + } + } + else if(!strcmp("%.*ls", fmt)) { + const wchar_t *wcs1 = va_arg(args, const wchar_t *); + const wchar_t *wcs2 = va_arg(args, const wchar_t *); + int len = va_arg(args, int); + is_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len); + + if (!is_equal) { + if (wcs1 && wcs2) { + int pos; + for (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos) + /* find differing byte offset */; + p_snprintf(buf, sizeof(buf), "'%.*ls' != '%.*ls' (at byte %d)", + len, wcs1, len, wcs2, pos); + } else { + p_snprintf(buf, sizeof(buf), "'%.*ls' != '%.*ls'", len, wcs1, len, wcs2); + } + } + } else if (!strcmp("%"PRIuZ, fmt) || !strcmp("%"PRIxZ, fmt)) { size_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t); is_equal = (sz1 == sz2); @@ -549,7 +778,7 @@ void clar__assert_equal( va_end(args); if (!is_equal) - clar__fail(file, line, err, buf, should_abort); + clar__fail(file, function, line, err, buf, should_abort); } void cl_set_cleanup(void (*cleanup)(void *), void *opaque) @@ -562,3 +791,4 @@ void cl_set_cleanup(void (*cleanup)(void *), void *opaque) #include "clar/fixtures.h" #include "clar/fs.h" #include "clar/print.h" +#include "clar/summary.h" diff --git a/tests/clar.h b/tests/clar.h index 8126305..8c22382 100644 --- a/tests/clar.h +++ b/tests/clar.h @@ -9,10 +9,24 @@ #include +enum cl_test_status { + CL_TEST_OK, + CL_TEST_FAILURE, + CL_TEST_SKIP, + CL_TEST_NOTRUN, +}; + +enum cl_output_format { + CL_OUTPUT_CLAP, + CL_OUTPUT_TAP, +}; + +/** Setup clar environment */ void clar_test_init(int argc, char *argv[]); int clar_test_run(void); void clar_test_shutdown(void); +/** One shot setup & run */ int clar_test(int argc, char *argv[]); const char *clar_sandbox_path(void); @@ -20,25 +34,68 @@ const char *clar_sandbox_path(void); void cl_set_cleanup(void (*cleanup)(void *), void *opaque); void cl_fs_cleanup(void); +/** + * cl_trace_* is a hook to provide a simple global tracing + * mechanism. + * + * The goal here is to let main() provide clar-proper + * with a callback to optionally write log info for + * test operations into the same stream used by their + * actual tests. This would let them print test names + * and maybe performance data as they choose. + * + * The goal is NOT to alter the flow of control or to + * override test selection/skipping. (So the callback + * does not return a value.) + * + * The goal is NOT to duplicate the existing + * pass/fail/skip reporting. (So the callback + * does not accept a status/errorcode argument.) + * + */ +typedef enum cl_trace_event { + CL_TRACE__SUITE_BEGIN, + CL_TRACE__SUITE_END, + CL_TRACE__TEST__BEGIN, + CL_TRACE__TEST__END, + CL_TRACE__TEST__RUN_BEGIN, + CL_TRACE__TEST__RUN_END, + CL_TRACE__TEST__LONGJMP, +} cl_trace_event; + +typedef void (cl_trace_cb)( + cl_trace_event ev, + const char *suite_name, + const char *test_name, + void *payload); + +/** + * Register a callback into CLAR to send global trace events. + * Pass NULL to disable. + */ +void cl_trace_register(cl_trace_cb *cb, void *payload); + + #ifdef CLAR_FIXTURE_PATH const char *cl_fixture(const char *fixture_name); void cl_fixture_sandbox(const char *fixture_name); void cl_fixture_cleanup(const char *fixture_name); +const char *cl_fixture_basename(const char *fixture_name); #endif /** * Assertion macros with explicit error message */ -#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __LINE__, "Function call failed: " #expr, desc, 1) -#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __LINE__, "Expected function call to fail: " #expr, desc, 1) -#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __LINE__, "Expression is not true: " #expr, desc, 1) +#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, "Function call failed: " #expr, desc, 1) +#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, "Expected function call to fail: " #expr, desc, 1) +#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, "Expression is not true: " #expr, desc, 1) /** * Check macros with explicit error message */ -#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __LINE__, "Function call failed: " #expr, desc, 0) -#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __LINE__, "Expected function call to fail: " #expr, desc, 0) -#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __LINE__, "Expression is not true: " #expr, desc, 0) +#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, "Function call failed: " #expr, desc, 0) +#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, "Expected function call to fail: " #expr, desc, 0) +#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, "Expression is not true: " #expr, desc, 0) /** * Assertion macros with no error message @@ -57,30 +114,40 @@ void cl_fixture_cleanup(const char *fixture_name); /** * Forced failure/warning */ -#define cl_fail(desc) clar__fail(__FILE__, __LINE__, "Test failed.", desc, 1) -#define cl_warning(desc) clar__fail(__FILE__, __LINE__, "Warning during test execution:", desc, 0) +#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, "Test failed.", desc, 1) +#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, "Warning during test execution:", desc, 0) + +#define cl_skip() clar__skip() /** * Typed assertion macros */ -#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%s", (s1), (s2)) -#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%s", (s1), (s2)) +#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%s", (s1), (s2)) +#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%s", (s1), (s2)) + +#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2, 1, "%ls", (wcs1), (wcs2)) +#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%ls", (wcs1), (wcs2)) + +#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%.*s", (s1), (s2), (int)(len)) +#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%.*s", (s1), (s2), (int)(len)) -#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%.*s", (s1), (s2), (int)(len)) -#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%.*s", (s1), (s2), (int)(len)) +#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2, 1, "%.*ls", (wcs1), (wcs2), (int)(len)) +#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%.*ls", (wcs1), (wcs2), (int)(len)) -#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__LINE__,#i1 " != " #i2, 1, "%d", (int)(i1), (int)(i2)) -#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__LINE__,#i1 " != " #i2 " (" #note ")", 1, "%d", (i1), (i2)) -#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__LINE__,#i1 " != " #i2, 1, (fmt), (int)(i1), (int)(i2)) +#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 " != " #i2, 1, "%d", (int)(i1), (int)(i2)) +#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 " != " #i2 " (" #note ")", 1, "%d", (i1), (i2)) +#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 " != " #i2, 1, (fmt), (int)(i1), (int)(i2)) -#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__LINE__,#b1 " != " #b2, 1, "%d", (int)((b1) != 0),(int)((b2) != 0)) +#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 " != " #b2, 1, "%d", (int)((b1) != 0),(int)((b2) != 0)) -#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__LINE__,"Pointer mismatch: " #p1 " != " #p2, 1, "%p", (p1), (p2)) +#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,"Pointer mismatch: " #p1 " != " #p2, 1, "%p", (p1), (p2)) +void clar__skip(void); void clar__fail( const char *file, - int line, + const char *func, + size_t line, const char *error, const char *description, int should_abort); @@ -88,14 +155,16 @@ void clar__fail( void clar__assert( int condition, const char *file, - int line, + const char *func, + size_t line, const char *error, const char *description, int should_abort); void clar__assert_equal( const char *file, - int line, + const char *func, + size_t line, const char *err, int should_abort, const char *fmt, diff --git a/tests/clar/fixtures.h b/tests/clar/fixtures.h index 264cd7f..6ec6423 100644 --- a/tests/clar/fixtures.h +++ b/tests/clar/fixtures.h @@ -1,3 +1,4 @@ +#ifdef CLAR_FIXTURE_PATH static const char * fixture_path(const char *base, const char *fixture_name) { @@ -20,7 +21,6 @@ fixture_path(const char *base, const char *fixture_name) return _path; } -#ifdef CLAR_FIXTURE_PATH const char *cl_fixture(const char *fixture_name) { return fixture_path(CLAR_FIXTURE_PATH, fixture_name); @@ -31,8 +31,20 @@ void cl_fixture_sandbox(const char *fixture_name) fs_copy(cl_fixture(fixture_name), _clar_path); } +const char *cl_fixture_basename(const char *fixture_name) +{ + const char *p; + + for (p = fixture_name; *p; p++) { + if (p[0] == '/' && p[1] && p[1] != '/') + fixture_name = p+1; + } + + return fixture_name; +} + void cl_fixture_cleanup(const char *fixture_name) { - fs_rm(fixture_path(_clar_path, fixture_name)); + fs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name))); } #endif diff --git a/tests/clar/fs.h b/tests/clar/fs.h index 7c7dde6..44ede45 100644 --- a/tests/clar/fs.h +++ b/tests/clar/fs.h @@ -1,5 +1,19 @@ +/* + * By default, use a read/write loop to copy files on POSIX systems. + * On Linux, use sendfile by default as it's slightly faster. On + * macOS, we avoid fcopyfile by default because it's slightly slower. + */ +#undef USE_FCOPYFILE +#define USE_SENDFILE 1 + #ifdef _WIN32 +#ifdef CLAR_WIN32_LONGPATHS +# define CLAR_MAX_PATH 4096 +#else +# define CLAR_MAX_PATH MAX_PATH +#endif + #define RM_RETRY_COUNT 5 #define RM_RETRY_DELAY 10 @@ -40,21 +54,48 @@ fs_rmdir_rmdir(WCHAR *_wpath) return 0; } +static void translate_path(WCHAR *path, size_t path_size) +{ + size_t path_len, i; + + if (wcsncmp(path, L"\\\\?\\", 4) == 0) + return; + + path_len = wcslen(path); + cl_assert(path_size > path_len + 4); + + for (i = path_len; i > 0; i--) { + WCHAR c = path[i - 1]; + + if (c == L'/') + path[i + 3] = L'\\'; + else + path[i + 3] = path[i - 1]; + } + + path[0] = L'\\'; + path[1] = L'\\'; + path[2] = L'?'; + path[3] = L'\\'; + path[path_len + 4] = L'\0'; +} + static void fs_rmdir_helper(WCHAR *_wsource) { - WCHAR buffer[MAX_PATH]; + WCHAR buffer[CLAR_MAX_PATH]; HANDLE find_handle; WIN32_FIND_DATAW find_data; size_t buffer_prefix_len; /* Set up the buffer and capture the length */ - wcscpy_s(buffer, MAX_PATH, _wsource); - wcscat_s(buffer, MAX_PATH, L"\\"); + wcscpy_s(buffer, CLAR_MAX_PATH, _wsource); + translate_path(buffer, CLAR_MAX_PATH); + wcscat_s(buffer, CLAR_MAX_PATH, L"\\"); buffer_prefix_len = wcslen(buffer); /* FindFirstFile needs a wildcard to match multiple items */ - wcscat_s(buffer, MAX_PATH, L"*"); + wcscat_s(buffer, CLAR_MAX_PATH, L"*"); find_handle = FindFirstFileW(buffer, &find_data); cl_assert(INVALID_HANDLE_VALUE != find_handle); @@ -64,7 +105,7 @@ fs_rmdir_helper(WCHAR *_wsource) if (fs__dotordotdot(find_data.cFileName)) continue; - wcscpy_s(buffer + buffer_prefix_len, MAX_PATH - buffer_prefix_len, find_data.cFileName); + wcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName); if (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes) fs_rmdir_helper(buffer); @@ -115,7 +156,7 @@ fs_rm_wait(WCHAR *_wpath) static void fs_rm(const char *_source) { - WCHAR wsource[MAX_PATH]; + WCHAR wsource[CLAR_MAX_PATH]; DWORD attrs; /* The input path is UTF-8. Convert it to wide characters @@ -125,7 +166,9 @@ fs_rm(const char *_source) _source, -1, /* Indicates NULL termination */ wsource, - MAX_PATH)); + CLAR_MAX_PATH)); + + translate_path(wsource, CLAR_MAX_PATH); /* Does the item exist? If not, we have no work to do */ attrs = GetFileAttributesW(wsource); @@ -150,21 +193,23 @@ fs_rm(const char *_source) static void fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest) { - WCHAR buf_source[MAX_PATH], buf_dest[MAX_PATH]; + WCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH]; HANDLE find_handle; WIN32_FIND_DATAW find_data; size_t buf_source_prefix_len, buf_dest_prefix_len; - wcscpy_s(buf_source, MAX_PATH, _wsource); - wcscat_s(buf_source, MAX_PATH, L"\\"); + wcscpy_s(buf_source, CLAR_MAX_PATH, _wsource); + wcscat_s(buf_source, CLAR_MAX_PATH, L"\\"); + translate_path(buf_source, CLAR_MAX_PATH); buf_source_prefix_len = wcslen(buf_source); - wcscpy_s(buf_dest, MAX_PATH, _wdest); - wcscat_s(buf_dest, MAX_PATH, L"\\"); + wcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest); + wcscat_s(buf_dest, CLAR_MAX_PATH, L"\\"); + translate_path(buf_dest, CLAR_MAX_PATH); buf_dest_prefix_len = wcslen(buf_dest); /* Get an enumerator for the items in the source. */ - wcscat_s(buf_source, MAX_PATH, L"*"); + wcscat_s(buf_source, CLAR_MAX_PATH, L"*"); find_handle = FindFirstFileW(buf_source, &find_data); cl_assert(INVALID_HANDLE_VALUE != find_handle); @@ -177,8 +222,8 @@ fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest) if (fs__dotordotdot(find_data.cFileName)) continue; - wcscpy_s(buf_source + buf_source_prefix_len, MAX_PATH - buf_source_prefix_len, find_data.cFileName); - wcscpy_s(buf_dest + buf_dest_prefix_len, MAX_PATH - buf_dest_prefix_len, find_data.cFileName); + wcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName); + wcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName); if (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes) fs_copydir_helper(buf_source, buf_dest); @@ -197,7 +242,7 @@ fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest) static void fs_copy(const char *_source, const char *_dest) { - WCHAR wsource[MAX_PATH], wdest[MAX_PATH]; + WCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH]; DWORD source_attrs, dest_attrs; HANDLE find_handle; WIN32_FIND_DATAW find_data; @@ -209,14 +254,17 @@ fs_copy(const char *_source, const char *_dest) _source, -1, wsource, - MAX_PATH)); + CLAR_MAX_PATH)); cl_assert(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, _dest, -1, wdest, - MAX_PATH)); + CLAR_MAX_PATH)); + + translate_path(wsource, CLAR_MAX_PATH); + translate_path(wdest, CLAR_MAX_PATH); /* Check the source for existence */ source_attrs = GetFileAttributesW(wsource); @@ -230,8 +278,8 @@ fs_copy(const char *_source, const char *_dest) * Use FindFirstFile to parse the path */ find_handle = FindFirstFileW(wsource, &find_data); cl_assert(INVALID_HANDLE_VALUE != find_handle); - wcscat_s(wdest, MAX_PATH, L"\\"); - wcscat_s(wdest, MAX_PATH, find_data.cFileName); + wcscat_s(wdest, CLAR_MAX_PATH, L"\\"); + wcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName); FindClose(find_handle); /* Check the new target for existence */ @@ -254,74 +302,213 @@ cl_fs_cleanup(void) #include #include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +# include +#endif -static int -shell_out(char * const argv[]) +#if defined(__APPLE__) +# include +#endif + +static void basename_r(const char **out, int *out_len, const char *in) { - int status, piderr; - pid_t pid; + size_t in_len = strlen(in), start_pos; + + for (in_len = strlen(in); in_len; in_len--) { + if (in[in_len - 1] != '/') + break; + } + + for (start_pos = in_len; start_pos; start_pos--) { + if (in[start_pos - 1] == '/') + break; + } - pid = fork(); + cl_assert(in_len - start_pos < INT_MAX); - if (pid < 0) { - fprintf(stderr, - "System error: `fork()` call failed (%d) - %s\n", - errno, strerror(errno)); - exit(-1); + if (in_len - start_pos > 0) { + *out = &in[start_pos]; + *out_len = (in_len - start_pos); + } else { + *out = "/"; + *out_len = 1; } +} + +static char *joinpath(const char *dir, const char *base, int base_len) +{ + char *out; + int len; + + if (base_len == -1) { + size_t bl = strlen(base); - if (pid == 0) { - execv(argv[0], argv); + cl_assert(bl < INT_MAX); + base_len = (int)bl; } - do { - piderr = waitpid(pid, &status, WUNTRACED); - } while (piderr < 0 && (errno == EAGAIN || errno == EINTR)); + len = strlen(dir) + base_len + 2; + cl_assert(len > 0); - return WEXITSTATUS(status); + cl_assert(out = malloc(len)); + cl_assert(snprintf(out, len, "%s/%.*s", dir, base_len, base) < len); + + return out; } static void -fs_copy(const char *_source, const char *dest) +fs_copydir_helper(const char *source, const char *dest, int dest_mode) { - char *argv[5]; - char *source; - size_t source_len; + DIR *source_dir; + struct dirent *d; - source = strdup(_source); - source_len = strlen(source); + mkdir(dest, dest_mode); - if (source[source_len - 1] == '/') - source[source_len - 1] = 0; + cl_assert_(source_dir = opendir(source), "Could not open source dir"); + while ((d = (errno = 0, readdir(source_dir))) != NULL) { + char *child; - argv[0] = "/bin/cp"; - argv[1] = "-R"; - argv[2] = source; - argv[3] = (char *)dest; - argv[4] = NULL; + if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) + continue; - cl_must_pass_( - shell_out(argv), - "Failed to copy test fixtures to sandbox" - ); + child = joinpath(source, d->d_name, -1); + fs_copy(child, dest); + free(child); + } + + cl_assert_(errno == 0, "Failed to iterate source dir"); - free(source); + closedir(source_dir); } static void -fs_rm(const char *source) +fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode) { - char *argv[4]; + int in, out; + + cl_must_pass((in = open(source, O_RDONLY))); + cl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode))); + +#if USE_FCOPYFILE && defined(__APPLE__) + ((void)(source_len)); /* unused */ + cl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA)); +#elif USE_SENDFILE && defined(__linux__) + { + ssize_t ret = 0; + + while (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) { + source_len -= (size_t)ret; + } + cl_assert(ret >= 0); + } +#else + { + char buf[131072]; + ssize_t ret; + + ((void)(source_len)); /* unused */ - argv[0] = "/bin/rm"; - argv[1] = "-Rf"; - argv[2] = (char *)source; - argv[3] = NULL; + while ((ret = read(in, buf, sizeof(buf))) > 0) { + size_t len = (size_t)ret; - cl_must_pass_( - shell_out(argv), - "Failed to cleanup the sandbox" - ); + while (len && (ret = write(out, buf, len)) > 0) { + cl_assert(ret <= (ssize_t)len); + len -= ret; + } + cl_assert(ret >= 0); + } + cl_assert(ret == 0); + } +#endif + + close(in); + close(out); +} + +static void +fs_copy(const char *source, const char *_dest) +{ + char *dbuf = NULL; + const char *dest = NULL; + struct stat source_st, dest_st; + + cl_must_pass_(lstat(source, &source_st), "Failed to stat copy source"); + + if (lstat(_dest, &dest_st) == 0) { + const char *base; + int base_len; + + /* Target exists and is directory; append basename */ + cl_assert(S_ISDIR(dest_st.st_mode)); + + basename_r(&base, &base_len, source); + cl_assert(base_len < INT_MAX); + + dbuf = joinpath(_dest, base, base_len); + dest = dbuf; + } else if (errno != ENOENT) { + cl_fail("Cannot copy; cannot stat destination"); + } else { + dest = _dest; + } + + if (S_ISDIR(source_st.st_mode)) { + fs_copydir_helper(source, dest, source_st.st_mode); + } else { + fs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode); + } + + free(dbuf); +} + +static void +fs_rmdir_helper(const char *path) +{ + DIR *dir; + struct dirent *d; + + cl_assert_(dir = opendir(path), "Could not open dir"); + while ((d = (errno = 0, readdir(dir))) != NULL) { + char *child; + + if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) + continue; + + child = joinpath(path, d->d_name, -1); + fs_rm(child); + free(child); + } + + cl_assert_(errno == 0, "Failed to iterate source dir"); + closedir(dir); + + cl_must_pass_(rmdir(path), "Could not remove directory"); +} + +static void +fs_rm(const char *path) +{ + struct stat st; + + if (lstat(path, &st)) { + if (errno == ENOENT) + return; + + cl_fail("Cannot copy; cannot stat destination"); + } + + if (S_ISDIR(st.st_mode)) { + fs_rmdir_helper(path); + } else { + cl_must_pass(unlink(path)); + } } void diff --git a/tests/clar/print.h b/tests/clar/print.h index 368016f..c17e2f6 100644 --- a/tests/clar/print.h +++ b/tests/clar/print.h @@ -1,28 +1,29 @@ +/* clap: clar protocol, the traditional clar output format */ -static void clar_print_init(int test_count, int suite_count, const char *suite_names) +static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names) { (void)test_count; printf("Loaded %d suites: %s\n", (int)suite_count, suite_names); - printf("Started\n"); + printf("Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\n"); } -static void clar_print_shutdown(int test_count, int suite_count, int error_count) +static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count) { (void)test_count; (void)suite_count; (void)error_count; printf("\n\n"); - clar_report_errors(); + clar_report_all(); } -static void clar_print_error(int num, const struct clar_error *error) +static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error) { printf(" %d) Failure:\n", num); - printf("%s::%s [%s:%d]\n", - error->suite, - error->test, + printf("%s::%s [%s:%"PRIuZ"]\n", + report->suite, + report->test, error->file, error->line_number); @@ -35,26 +36,176 @@ static void clar_print_error(int num, const struct clar_error *error) fflush(stdout); } -static void clar_print_ontest(const char *test_name, int test_number, int failed) +static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status) { (void)test_name; (void)test_number; - printf("%c", failed ? 'F' : '.'); - fflush(stdout); + + if (_clar.verbosity > 1) { + printf("%s::%s: ", suite_name, test_name); + + switch (status) { + case CL_TEST_OK: printf("ok\n"); break; + case CL_TEST_FAILURE: printf("fail\n"); break; + case CL_TEST_SKIP: printf("skipped"); break; + case CL_TEST_NOTRUN: printf("notrun"); break; + } + } else { + switch (status) { + case CL_TEST_OK: printf("."); break; + case CL_TEST_FAILURE: printf("F"); break; + case CL_TEST_SKIP: printf("S"); break; + case CL_TEST_NOTRUN: printf("N"); break; + } + + fflush(stdout); + } } -static void clar_print_onsuite(const char *suite_name, int suite_index) +static void clar_print_clap_onsuite(const char *suite_name, int suite_index) { - if (_clar.report_suite_names) + if (_clar.verbosity == 1) printf("\n%s", suite_name); (void)suite_index; } +static void clar_print_clap_onabort(const char *fmt, va_list arg) +{ + vfprintf(stderr, fmt, arg); +} + +/* tap: test anywhere protocol format */ + +static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names) +{ + (void)test_count; + (void)suite_count; + (void)suite_names; + printf("TAP version 13\n"); +} + +static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count) +{ + (void)suite_count; + (void)error_count; + + printf("1..%d\n", test_count); +} + +static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error) +{ + (void)num; + (void)report; + (void)error; +} + +static void print_escaped(const char *str) +{ + char *c; + + while ((c = strchr(str, '\'')) != NULL) { + printf("%.*s", (int)(c - str), str); + printf("''"); + str = c + 1; + } + + printf("%s", str); +} + +static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status) +{ + const struct clar_error *error = _clar.last_report->errors; + + (void)test_name; + (void)test_number; + + switch(status) { + case CL_TEST_OK: + printf("ok %d - %s::%s\n", test_number, suite_name, test_name); + break; + case CL_TEST_FAILURE: + printf("not ok %d - %s::%s\n", test_number, suite_name, test_name); + + printf(" ---\n"); + printf(" reason: |\n"); + printf(" %s\n", error->error_msg); + + if (error->description) + printf(" %s\n", error->description); + + printf(" at:\n"); + printf(" file: '"); print_escaped(error->file); printf("'\n"); + printf(" line: %" PRIuZ "\n", error->line_number); + printf(" function: '%s'\n", error->function); + printf(" ---\n"); + + break; + case CL_TEST_SKIP: + case CL_TEST_NOTRUN: + printf("ok %d - # SKIP %s::%s\n", test_number, suite_name, test_name); + break; + } + + fflush(stdout); +} + +static void clar_print_tap_onsuite(const char *suite_name, int suite_index) +{ + printf("# start of suite %d: %s\n", suite_index, suite_name); +} + +static void clar_print_tap_onabort(const char *fmt, va_list arg) +{ + printf("Bail out! "); + vprintf(fmt, arg); + fflush(stdout); +} + +/* indirection between protocol output selection */ + +#define PRINT(FN, ...) do { \ + switch (_clar.output_format) { \ + case CL_OUTPUT_CLAP: \ + clar_print_clap_##FN (__VA_ARGS__); \ + break; \ + case CL_OUTPUT_TAP: \ + clar_print_tap_##FN (__VA_ARGS__); \ + break; \ + default: \ + abort(); \ + } \ + } while (0) + +static void clar_print_init(int test_count, int suite_count, const char *suite_names) +{ + PRINT(init, test_count, suite_count, suite_names); +} + +static void clar_print_shutdown(int test_count, int suite_count, int error_count) +{ + PRINT(shutdown, test_count, suite_count, error_count); +} + +static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error) +{ + PRINT(error, num, report, error); +} + +static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status) +{ + PRINT(ontest, suite_name, test_name, test_number, status); +} + +static void clar_print_onsuite(const char *suite_name, int suite_index) +{ + PRINT(onsuite, suite_name, suite_index); +} + static void clar_print_onabort(const char *msg, ...) { va_list argp; va_start(argp, msg); - vfprintf(stderr, msg, argp); + PRINT(onabort, msg, argp); va_end(argp); } diff --git a/tests/clar/sandbox.h b/tests/clar/sandbox.h index a44e291..0ba1479 100644 --- a/tests/clar/sandbox.h +++ b/tests/clar/sandbox.h @@ -1,4 +1,8 @@ -static char _clar_path[4096]; +#ifdef __APPLE__ +#include +#endif + +static char _clar_path[4096 + 1]; static int is_valid_tmp_path(const char *path) @@ -31,14 +35,24 @@ find_tmp_path(char *buffer, size_t length) continue; if (is_valid_tmp_path(env)) { - strncpy(buffer, env, length); +#ifdef __APPLE__ + if (length >= PATH_MAX && realpath(env, buffer) != NULL) + return 0; +#endif + strncpy(buffer, env, length - 1); + buffer[length - 1] = '\0'; return 0; } } /* If the environment doesn't say anything, try to use /tmp */ if (is_valid_tmp_path("/tmp")) { - strncpy(buffer, "/tmp", length); +#ifdef __APPLE__ + if (length >= PATH_MAX && realpath("/tmp", buffer) != NULL) + return 0; +#endif + strncpy(buffer, "/tmp", length - 1); + buffer[length - 1] = '\0'; return 0; } @@ -53,7 +67,8 @@ find_tmp_path(char *buffer, size_t length) /* This system doesn't like us, try to use the current directory */ if (is_valid_tmp_path(".")) { - strncpy(buffer, ".", length); + strncpy(buffer, ".", length - 1); + buffer[length - 1] = '\0'; return 0; } @@ -65,14 +80,19 @@ static void clar_unsandbox(void) if (_clar_path[0] == '\0') return; - chdir(".."); + cl_must_pass(chdir("..")); fs_rm(_clar_path); } static int build_sandbox_path(void) { +#ifdef CLAR_TMPDIR + const char path_tail[] = CLAR_TMPDIR "_XXXXXX"; +#else const char path_tail[] = "clar_tmp_XXXXXX"; +#endif + size_t len; if (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0) diff --git a/tests/clar/summary.h b/tests/clar/summary.h new file mode 100644 index 0000000..1af110e --- /dev/null +++ b/tests/clar/summary.h @@ -0,0 +1,134 @@ + +#include +#include + +int clar_summary_close_tag( + struct clar_summary *summary, const char *tag, int indent) +{ + const char *indt; + + if (indent == 0) indt = ""; + else if (indent == 1) indt = "\t"; + else indt = "\t\t"; + + return fprintf(summary->fp, "%s\n", indt, tag); +} + +int clar_summary_testsuites(struct clar_summary *summary) +{ + return fprintf(summary->fp, "\n"); +} + +int clar_summary_testsuite(struct clar_summary *summary, + int idn, const char *name, const char *pkg, time_t timestamp, + double elapsed, int test_count, int fail_count, int error_count) +{ + struct tm *tm = localtime(×tamp); + char iso_dt[20]; + + if (strftime(iso_dt, sizeof(iso_dt), "%Y-%m-%dT%H:%M:%S", tm) == 0) + return -1; + + return fprintf(summary->fp, "\t\n", + idn, name, pkg, iso_dt, elapsed, test_count, fail_count, error_count); +} + +int clar_summary_testcase(struct clar_summary *summary, + const char *name, const char *classname, double elapsed) +{ + return fprintf(summary->fp, + "\t\t\n", + name, classname, elapsed); +} + +int clar_summary_failure(struct clar_summary *summary, + const char *type, const char *message, const char *desc) +{ + return fprintf(summary->fp, + "\t\t\t\n", + type, message, desc); +} + +struct clar_summary *clar_summary_init(const char *filename) +{ + struct clar_summary *summary; + FILE *fp; + + if ((fp = fopen(filename, "w")) == NULL) + return NULL; + + if ((summary = malloc(sizeof(struct clar_summary))) == NULL) { + fclose(fp); + return NULL; + } + + summary->filename = filename; + summary->fp = fp; + + return summary; +} + +int clar_summary_shutdown(struct clar_summary *summary) +{ + struct clar_report *report; + const char *last_suite = NULL; + + if (clar_summary_testsuites(summary) < 0) + goto on_error; + + report = _clar.reports; + while (report != NULL) { + struct clar_error *error = report->errors; + + if (last_suite == NULL || strcmp(last_suite, report->suite) != 0) { + if (clar_summary_testsuite(summary, 0, report->suite, "", + time(NULL), 0, _clar.tests_ran, _clar.total_errors, 0) < 0) + goto on_error; + } + + last_suite = report->suite; + + clar_summary_testcase(summary, report->test, "what", 0); + + while (error != NULL) { + if (clar_summary_failure(summary, "assert", + error->error_msg, error->description) < 0) + goto on_error; + + error = error->next; + } + + if (clar_summary_close_tag(summary, "testcase", 2) < 0) + goto on_error; + + report = report->next; + + if (!report || strcmp(last_suite, report->suite) != 0) { + if (clar_summary_close_tag(summary, "testsuite", 1) < 0) + goto on_error; + } + } + + if (clar_summary_close_tag(summary, "testsuites", 0) < 0 || + fclose(summary->fp) != 0) + goto on_error; + + printf("written summary file to %s\n", summary->filename); + + free(summary); + return 0; + +on_error: + fclose(summary->fp); + free(summary); + return -1; +} diff --git a/tests/generate.py b/tests/generate.py index d4fe8f2..931b4d6 100644 --- a/tests/generate.py +++ b/tests/generate.py @@ -8,7 +8,7 @@ from __future__ import with_statement from string import Template -import re, fnmatch, os, codecs, pickle +import re, fnmatch, os, sys, codecs, pickle class Module(object): class Template(object): @@ -24,8 +24,8 @@ class DeclarationTemplate(Template): def render(self): out = "\n".join("extern %s;" % cb['declaration'] for cb in self.module.callbacks) + "\n" - if self.module.initialize: - out += "extern %s;\n" % self.module.initialize['declaration'] + for initializer in self.module.initializers: + out += "extern %s;\n" % initializer['declaration'] if self.module.cleanup: out += "extern %s;\n" % self.module.cleanup['declaration'] @@ -41,7 +41,19 @@ def render(self): class InfoTemplate(Template): def render(self): - return Template( + templates = [] + + initializers = self.module.initializers + if len(initializers) == 0: + initializers = [ None ] + + for initializer in initializers: + name = self.module.clean_name() + if initializer and initializer['short_name'].startswith('initialize_'): + variant = initializer['short_name'][len('initialize_'):] + name += " (%s)" % variant.replace('_', ' ') + + template = Template( r""" { "${clean_name}", @@ -49,19 +61,22 @@ def render(self): ${cleanup}, ${cb_ptr}, ${cb_count}, ${enabled} }""" - ).substitute( - clean_name = self.module.clean_name(), - initialize = self._render_callback(self.module.initialize), - cleanup = self._render_callback(self.module.cleanup), - cb_ptr = "_clar_cb_%s" % self.module.name, - cb_count = len(self.module.callbacks), - enabled = int(self.module.enabled) - ) + ).substitute( + clean_name = name, + initialize = self._render_callback(initializer), + cleanup = self._render_callback(self.module.cleanup), + cb_ptr = "_clar_cb_%s" % self.module.name, + cb_count = len(self.module.callbacks), + enabled = int(self.module.enabled) + ) + templates.append(template) + + return ','.join(templates) def __init__(self, name): self.name = name - self.mtime = 0 + self.mtime = None self.enabled = True self.modified = False @@ -80,13 +95,13 @@ def _replacer(match): return re.sub(SKIP_COMMENTS_REGEX, _replacer, text) def parse(self, contents): - TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\(\s*void\s*\))\s*\{" + TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\s*\(\s*void\s*\))\s*\{" contents = self._skip_comments(contents) regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE) self.callbacks = [] - self.initialize = None + self.initializers = [] self.cleanup = None for (declaration, symbol, short_name) in regex.findall(contents): @@ -96,8 +111,8 @@ def parse(self, contents): "symbol" : symbol } - if short_name == 'initialize': - self.initialize = data + if short_name.startswith('initialize'): + self.initializers.append(data) elif short_name == 'cleanup': self.cleanup = data else: @@ -118,7 +133,7 @@ def refresh(self, path): self.modified = True self.mtime = st.st_mtime - with open(path) as fp: + with codecs.open(path, encoding='utf-8') as fp: raw_content = fp.read() except IOError: @@ -128,8 +143,9 @@ def refresh(self, path): class TestSuite(object): - def __init__(self, path): + def __init__(self, path, output): self.path = path + self.output = output def should_generate(self, path): if not os.path.isfile(path): @@ -150,14 +166,14 @@ def find_modules(self): for test_file in tests_in_module: full_path = os.path.join(root, test_file) - module_name = "_".join(module_root + [test_file[:-2]]) + module_name = "_".join(module_root + [test_file[:-2]]).replace("-", "_") modules.append((full_path, module_name)) return modules def load_cache(self): - path = os.path.join(self.path, '.clarcache') + path = os.path.join(self.output, '.clarcache') cache = {} try: @@ -170,7 +186,7 @@ def load_cache(self): return cache def save_cache(self): - path = os.path.join(self.path, '.clarcache') + path = os.path.join(self.output, '.clarcache') with open(path, 'wb') as cache: pickle.dump(self.modules, cache) @@ -194,28 +210,30 @@ def disable(self, excluded): module.modified = True def suite_count(self): - return len(self.modules) + return sum(max(1, len(m.initializers)) for m in self.modules.values()) def callback_count(self): return sum(len(module.callbacks) for module in self.modules.values()) def write(self): - output = os.path.join(self.path, 'clar.suite') + output = os.path.join(self.output, 'clar.suite') if not self.should_generate(output): return False with open(output, 'w') as data: - for module in self.modules.values(): + modules = sorted(self.modules.values(), key=lambda module: module.name) + + for module in modules: t = Module.DeclarationTemplate(module) data.write(t.render()) - for module in self.modules.values(): + for module in modules: t = Module.CallbacksTemplate(module) data.write(t.render()) suites = "static struct clar_suite _clar_suites[] = {" + ','.join( - Module.InfoTemplate(module).render() for module in sorted(self.modules.values(), key=lambda module: module.name) + Module.InfoTemplate(module).render() for module in modules ) + "\n};\n" data.write(suites) @@ -223,22 +241,27 @@ def write(self): data.write("static const size_t _clar_suite_count = %d;\n" % self.suite_count()) data.write("static const size_t _clar_callback_count = %d;\n" % self.callback_count()) - suite.save_cache() + self.save_cache() return True if __name__ == '__main__': from optparse import OptionParser parser = OptionParser() - parser.add_option('-f', '--force', dest='force', default=False) + parser.add_option('-f', '--force', action="store_true", dest='force', default=False) parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[]) + parser.add_option('-o', '--output', dest='output') options, args = parser.parse_args() - - for path in args or ['.']: - suite = TestSuite(path) - suite.load(options.force) - suite.disable(options.excluded) - if suite.write(): - print("Written `clar.suite` (%d tests in %d suites)" % (suite.callback_count(), suite.suite_count())) + if len(args) > 1: + print("More than one path given") + sys.exit(1) + + path = args.pop() if args else '.' + output = options.output or path + suite = TestSuite(path, output) + suite.load(options.force) + suite.disable(options.excluded) + if suite.write(): + print("Written `clar.suite` (%d tests in %d suites)" % (suite.callback_count(), suite.suite_count()))