#include #include "xtest.h" #include struct xtest_assert_info { const char *file; int line; const char *func; const char *expr; }; struct xtest_assert_info assertion_info; #ifndef XTEST_MAX_GROUP_NESTING #define XTEST_MAX_GROUP_NESTING 16 #endif #ifndef XTEST_TEST_NAME_MAX_LENGTH #define XTEST_TEST_NAME_MAX_LENGTH 1023 #endif #ifndef XTEST_INDENT #define XTEST_INDENT 2 #endif const char *group_nesting[XTEST_MAX_GROUP_NESTING]; int group_nesting_pos = 0; jmp_buf xtest_jmp; #ifndef XTEST_ASSERT_MESSAGE_MAX_LEN #define XTEST_ASSERT_MESSAGE_MAX_LEN 1023 #endif char assert_message[XTEST_ASSERT_MESSAGE_MAX_LEN + 1]; 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; 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; } void subunit_message(const char *name, const char *message, const char *details) { printf("%s: %s", message, name); if (details == NULL) { printf("\n"); } else { printf(" [\n%s%s]\n", details, details[strlen(details)] != '\n' ? "\n" : ""); } } 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] = ""; size_t name_len = 0; for (int i = 0; i < group_nesting_pos; ++i) { name_len += snprintf(full_name + name_len, XTEST_TEST_NAME_MAX_LENGTH - name_len, "%s/", group_nesting[i]); assert(name_len <= XTEST_TEST_NAME_MAX_LENGTH); } name_len += snprintf(full_name + name_len, XTEST_TEST_NAME_MAX_LENGTH - name_len, "%s", name); assert(name_len <= XTEST_TEST_NAME_MAX_LENGTH); 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 (setup != NULL) { int jmpres = setjmp(xtest_jmp); if (jmpres == 0) { setup(&fixture); } else if (jmpres == 3) { goto skipped; } else { snprintf(assert_message, 1024, "assertion failure in setup function: %s in %s (%s:%d)", assertion_info.expr, assertion_info.func, assertion_info.file, assertion_info.line); goto failed; } } int jmpres = setjmp(xtest_jmp); if (jmpres == 0) { if (params == NULL) { expecting_assertion = 0; fn(fixture, NULL); } else { int num_params = 0; while (params[num_params].name != NULL) num_params++; int validx[num_params]; int valmax[num_params]; void *current_vals[num_params]; for (int i = 0; i < num_params; ++i) { validx[i] = 0; valmax[i] = 0; while ((*params[i].values)[valmax[i]] != NULL) valmax[i]++; } while (1) { for (int i = 0; i < num_params; ++i) { current_vals[i] = (*params[i].values)[validx[i]]; } expecting_assertion = 0; fn(fixture, current_vals); int carry = 1; for (int i = num_params - 1; i >= 0; --i) { validx[i] += 1; if (validx[i] >= valmax[i]) { validx[i] = 0; } else { carry = 0; break; } } if (carry == 1) break; } } if (expecting_assertion) { // an assertion failure in the tested code was expected but didn't occur snprintf(assert_message, 1024, "expected an assertion failure in the tested code"); goto failed; } else { goto success; } } else if (jmpres == 1) { // a testing assertion failed goto failed; } else if (jmpres == 2) { // an assertion in the tested code failed if (expecting_assertion) { goto success; } else { snprintf(assert_message, 1024, "unexpected assertion failure in tested code: %s in %s (%s:%d)", assertion_info.expr, assertion_info.func, assertion_info.file, assertion_info.line); goto failed; } } else if (jmpres == 3) { goto skipped; } success: #ifndef XTEST_ENABLE_SUBUNIT printf("SUCCESS\n"); #else subunit_message(full_name, "success", NULL); #endif //XTEST_ENABLE_SUBUNIT 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 failed_tests += 1; goto cleanup; skipped: skipped_tests += 1; #ifndef XTEST_ENABLE_SUBUNIT 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: if (teardown != NULL && fixture != NULL) { teardown(fixture); } } /// this function replaces the standard library assert and jumps back to the xtest_internal_run invocation that is running the test /// \param file file in which the assertion happened /// \param line line on which the assertion happened /// \param func function in which the assertion happened /// \param expr stringified expression of the assertion void xtest_internal_assert(const char *file, int line, const char *func, const char *expr) { assertion_info = (struct xtest_assert_info) { .file = file, .line = line, .func = func, .expr = expr }; longjmp(xtest_jmp, 2); } #define FAIL_ASSERT_FORMAT_1 "assertion failed: %s, expected %s" #define FAIL_ASSERT_FORMAT_2 " but got " #define FAIL_ASSERT_FORMAT_3 " (%s:%d)" #define FAIL_ASSERT_FORMAT(fmt) FAIL_ASSERT_FORMAT_1 fmt FAIL_ASSERT_FORMAT_2 fmt FAIL_ASSERT_FORMAT_3 #define CAST_PTR(type, ptr) *(type *)(ptr) #define PRINT_ASSERT_MESSAGE(fmt, type) snprintf(assert_message, XTEST_ASSERT_MESSAGE_MAX_LEN, FAIL_ASSERT_FORMAT(fmt), \ expression, invert ? "not " : "", CAST_PTR(type, expected), CAST_PTR(type, actual), file, line) void xtest_fail_assert(const char *expression, const char *file, int line, void *expected, void *actual, int invert, enum xtest_type type) { switch (type) { case xtest_type_bool: snprintf(assert_message, XTEST_ASSERT_MESSAGE_MAX_LEN, FAIL_ASSERT_FORMAT("%s"), expression, invert ? "not " : "", CAST_PTR(_Bool, expected) ? "true" : "false", CAST_PTR(_Bool, actual) ? "true" : "false", file, line); break; case xtest_type_char: PRINT_ASSERT_MESSAGE("'%c'", char); break; case xtest_type_short: PRINT_ASSERT_MESSAGE("%i", short); break; case xtest_type_unsigned_short: PRINT_ASSERT_MESSAGE("%u", unsigned short); break; case xtest_type_int: PRINT_ASSERT_MESSAGE("%i", int); break; case xtest_type_unsigned_int: PRINT_ASSERT_MESSAGE("%u", unsigned int); break; case xtest_type_long: PRINT_ASSERT_MESSAGE("%li", long); break; case xtest_type_unsigned_long: PRINT_ASSERT_MESSAGE("%lu", unsigned long); break; case xtest_type_long_long: PRINT_ASSERT_MESSAGE("%lli", long long); break; case xtest_type_unsigned_long_long: PRINT_ASSERT_MESSAGE("%llu", unsigned long long); break; case xtest_type_float: PRINT_ASSERT_MESSAGE("%f", float); break; case xtest_type_double: PRINT_ASSERT_MESSAGE("%f", double); break; case xtest_type_long_double: PRINT_ASSERT_MESSAGE("%Lf", long double); break; case xtest_type_string: PRINT_ASSERT_MESSAGE("\"%s\"", char*); break; case xtest_type_void_pointer: PRINT_ASSERT_MESSAGE("%p", void*); break; case xtest_type_other: default: snprintf(assert_message, XTEST_ASSERT_MESSAGE_MAX_LEN, FAIL_ASSERT_FORMAT("%s"), expression, invert ? "not " : "", "", "", file, line); break; } longjmp(xtest_jmp, 1); } void xtest_assert_float(double expected, double actual, int precision, int invert, const char *expression, const char *file, int line) { double epsilon = 1.0; for (int i = 0; i < precision; ++i) epsilon /= 10.0; double diff = expected - actual; if (diff < 0) diff = -diff; int equals = diff < epsilon; int failed = invert == equals; // invert = true: fail if equals, invert = false: fail if not equals if (!failed) return; snprintf(assert_message, 1024, "assertion failed: %s, expected %s%g but got %g (difference %g, precision %d) (%s:%d)", expression, invert ? "not " : "", expected, actual, diff, precision, file, line); longjmp(xtest_jmp, 1); } size_t find_diff_offset(const char *a, const char *b, size_t length) { size_t offset = 0; while (offset < length && *(a + offset) == *(b + offset)) offset++; return offset; } void xtest_assert_mem(const char *expected, const char *actual, size_t length, int invert, const char *expression, const char *file, int line) { size_t offset = find_diff_offset(expected, actual, length); int equals = offset == length; int failed = invert == equals; // invert = true: fail if equals, invert = false: fail if not equals if (!failed) return; if (invert) { snprintf(assert_message, 1024, "assertion failed: %s, expected memory to not match but did (%s:%d)", expression, file, line); } else { snprintf(assert_message, 1024, "assertion failed: %s, expected memory to match but did not (first different byte at offset %zu, expected 0x%2x but got 0x%2x) (%s:%d)", expression, offset, (unsigned int) expected[offset], (unsigned int) actual[offset], file, line); } longjmp(xtest_jmp, 1); } void xtest_skip(const char *reason) { skip_reason = reason; longjmp(xtest_jmp, 3); } void xtest_internal_start_group(const char *name) { #ifndef XTEST_ENABLE_SUBUNIT printf("%*s%s:\n", xtest_indent, "", name); #endif group_nesting[group_nesting_pos] = name; group_nesting_pos += 1; xtest_indent += XTEST_INDENT; } void xtest_internal_end_group() { assert(group_nesting_pos > 0); group_nesting_pos -= 1; xtest_indent -= XTEST_INDENT; } void xtest_expect_assertion_failure() { expecting_assertion = 1; }