diff --git a/xtest.c b/xtest.c index 3b0209e..032f27f 100644 --- a/xtest.c +++ b/xtest.c @@ -1,6 +1,8 @@ #include +#include #include "xtest.h" #include +#include struct xtest_assert_info { const char *file; @@ -11,7 +13,6 @@ struct xtest_assert_info { struct xtest_assert_info assertion_info; - #ifndef XTEST_MAX_GROUP_NESTING #define XTEST_MAX_GROUP_NESTING 16 #endif @@ -24,6 +25,16 @@ struct xtest_assert_info assertion_info; #define XTEST_INDENT 2 #endif + +enum output_format { + format_default, + format_subunit, +}; + +enum output_format output_format; + +const char *name_filter; + const char *group_nesting[XTEST_MAX_GROUP_NESTING]; int group_nesting_pos = 0; @@ -37,20 +48,131 @@ const char *skip_reason; int xtest_indent = 0; - int expecting_assertion = 0; int num_tests = 0; int successful_tests = 0; int failed_tests = 0; int skipped_tests = 0; +_Bool list_tests = 0; + + +#define USAGE_BASE "[-?|--help] [-l|--list] [-m|--match filter] [-f|--output-format format]" + +#define USAGE_HELP_LIST "-l|--list list the available tests" +#define USAGE_HELP_MATCH "-m|--match filter only run/list tests whose name matches the filter" +#define USAGE_HELP_FORMAT "-f|--output-format format specify the output format; supported formats are \"default\" and \"subunit\"" +#define USAGE_HELP_HELP "-?|--help show this message" + +#define USAGE "Usage: %s " USAGE_BASE "\n " USAGE_HELP_LIST "\n " USAGE_HELP_MATCH "\n " USAGE_HELP_FORMAT "\n " USAGE_HELP_HELP + + +static _Noreturn void usage(const char *progname) { + printf(USAGE, progname); + exit(1); +} + +static _Bool matches(const char *name, const char *filter) { + // eat up the matching parts of name and filter until the first * or end of either string + while (*name == *filter && *name != '\0' && *filter != '\0' && *filter != '*') { + name++; + filter++; + } + // if the next character in filter is not a * then there is no match (otherwise the previous loop would have continued) + if (*filter != '\0' && *filter != '*') return 0; + + while (1) { + // if the filter ends before the name then they don't match + if (*filter == '\0') { + return *name == '\0'; + } + // the filter points to a * at this point because everything else was eaten up either before the loop or in the previous iteration of the loop + assert (*filter == '*'); + while (*filter == '*') filter++; // eat up consecutive *s + size_t part_len = strcspn(filter, "*"); + if (part_len == 0) return 1; // filter ends with a * so every remaining input in name is accepted + char part[part_len + 1]; + strncpy(part, filter, part_len); + part[part_len] = '\0'; // ensure null-termination since strncpy does not null-terminate on its own + name = strstr(name, part); + if (name == NULL) return 0; + name += part_len; + filter += part_len; + } +} + +void xtest_init(int argc, char **argv) { + int c; + int list_flag = 0; + int help_flag = 0; + const char *format = NULL; + + const char *progname = argv[0]; + + struct option long_options[] = + { + {"list", no_argument, &list_flag, 1}, // list the tests instead of running them + {"help", no_argument, &help_flag, 1}, // list the tests instead of running them + {"match", required_argument, 0, 'm'}, // only run tests that match the provided filter + {"output-format", required_argument, 0, 'f'}, // select output format + {0, 0, 0, 0} + }; + while (1) { + int option_index = 0; + c = getopt_long(argc, argv, "?lm:f:", long_options, &option_index); + if (c == -1) break; + switch (c) { + case 0: + assert(long_options[option_index].flag != 0); + break; + case 'l': + list_flag = 1; + break; + case 'm': + name_filter = optarg; + break; + case 'f': + format = optarg; + break; + case '?': + help_flag = 1; + break; + default: + assert(0); + } + } + + if (help_flag) { + usage(progname); + } + + if (list_flag) + list_tests = 1; + + if (format == NULL) { + format = "default"; + } + if (strcmp(format, "default") == 0) { + output_format = format_default; + } else if (strcmp(format, "subunit") == 0) { + output_format = format_subunit; + } else { + printf("invalid output format '%s', allowed formats are 'default' and 'subunit'\n", format); + exit(1); + } +} + int xtest_complete() { -#ifndef XTEST_ENABLE_SUBUNIT - printf("============\nTotal: %d, Failed: %d, Skipped: %d\n", num_tests, failed_tests, skipped_tests); -#endif //XTEST_ENABLE_SUBUNIT - fflush(stdout); - return failed_tests != 0 ? 1 : 0; + if (!list_tests) { + if (output_format == format_default) { + printf("============\nTotal: %d, Failed: %d, Skipped: %d\n", num_tests, failed_tests, skipped_tests); + } + fflush(stdout); + return failed_tests != 0 ? 1 : 0; + } else { + return 0; + } } void subunit_message(const char *name, const char *message, const char *details) { @@ -64,7 +186,6 @@ void subunit_message(const char *name, const char *message, const char *details) void xtest_internal_run(xtest_test_fn fn, const char *name, xtest_param *params, xtest_setup_fn setup, xtest_teardown_fn teardown) { - num_tests += 1; expecting_assertion = 0; char full_name[XTEST_TEST_NAME_MAX_LENGTH + 1] = ""; @@ -76,14 +197,25 @@ void xtest_internal_run(xtest_test_fn fn, const char *name, xtest_param *params, name_len += snprintf(full_name + name_len, XTEST_TEST_NAME_MAX_LENGTH - name_len, "%s", name); assert(name_len <= XTEST_TEST_NAME_MAX_LENGTH); + if (name_filter != NULL) { + if (!matches(full_name, name_filter)) return; + } + + if (list_tests) { + printf("%s\n", full_name); + return; + } + + num_tests += 1; + + void *fixture = NULL; -#ifndef XTEST_ENABLE_SUBUNIT - printf("%*s%s: ", xtest_indent, "", name); -#else - subunit_message(full_name, "test", NULL); -#endif //XTEST_ENABLE_SUBUNIT - + if (output_format == format_subunit) { + subunit_message(full_name, "test", NULL); + } else { + printf("%*s%s: ", xtest_indent, "", name); + } if (setup != NULL) { int jmpres = setjmp(xtest_jmp); if (jmpres == 0) { @@ -154,32 +286,35 @@ void xtest_internal_run(xtest_test_fn fn, const char *name, xtest_param *params, } success: -#ifndef XTEST_ENABLE_SUBUNIT - printf("SUCCESS\n"); -#else - subunit_message(full_name, "success", NULL); -#endif //XTEST_ENABLE_SUBUNIT + + if (output_format == format_subunit) { + subunit_message(full_name, "success", NULL); + } else { + printf("SUCCESS\n"); + } successful_tests += 1; goto cleanup; failed: -#ifndef XTEST_ENABLE_SUBUNIT - printf("FAILED:\n%*s %s\n", xtest_indent, "", assert_message); -#else - subunit_message(full_name, "failure", assert_message); -#endif //XTEST_ENABLE_SUBUNIT + + if (output_format == format_subunit) { + subunit_message(full_name, "failure", assert_message); + } else { + printf("FAILED:\n%*s %s\n", xtest_indent, "", assert_message); + } failed_tests += 1; goto cleanup; skipped: skipped_tests += 1; -#ifndef XTEST_ENABLE_SUBUNIT - if (skip_reason != NULL) { - printf("SKIPPED: %s\n", skip_reason); + + if (output_format == format_subunit) { + subunit_message(full_name, "skipped", skip_reason); } else { - printf("SKIPPED\n"); + if (skip_reason != NULL) { + printf("SKIPPED: %s\n", skip_reason); + } else { + printf("SKIPPED\n"); + } } -#else - subunit_message(full_name, "skipped", skip_reason); -#endif //XTEST_ENABLE_SUBUNIT goto cleanup; cleanup: @@ -321,9 +456,11 @@ void xtest_skip(const char *reason) { } void xtest_internal_start_group(const char *name) { -#ifndef XTEST_ENABLE_SUBUNIT - printf("%*s%s:\n", xtest_indent, "", name); -#endif + if (!list_tests) { + if (output_format == format_default) { + printf("%*s%s:\n", xtest_indent, "", name); + } + } group_nesting[group_nesting_pos] = name; group_nesting_pos += 1; xtest_indent += XTEST_INDENT; @@ -337,4 +474,4 @@ void xtest_internal_end_group() { void xtest_expect_assertion_failure() { expecting_assertion = 1; -} +} \ No newline at end of file diff --git a/xtest.h b/xtest.h index 9962305..f132fd1 100644 --- a/xtest.h +++ b/xtest.h @@ -42,6 +42,7 @@ enum xtest_type { xtest_type_other, }; +void xtest_init(int argc, char**argv); void xtest_fail_assert(const char *expression, const char *file, int line, void *expected, void *actual, _Bool invert, enum xtest_type type); @@ -129,6 +130,7 @@ int xtest_complete(); #define XTEST_RUN_MAIN(fn) int main(int argc, char ** argv) { \ + xtest_init(argc, argv); \ fn(); \ return xtest_complete(); \ }