#include #include #include #include #include #include #define TEST_INTERNAL #include "test.h" typedef u32 et_test_status; #define test_current(_plan) ((_plan)->tests + (_plan)->current_test) static int crit_test_fail_init(const struct et_state *state, void *data, et_state_init_data init_data) { struct et_test_plan *plan = init_data.p; struct et_test_plan_entry *last_ent; *(void **)data = plan; assert(plan->current_test > 0); /* NOTE: current_test has been incremented by this point */ last_ent = plan->tests + (plan->current_test - 1); printf("\nCritical test failed: [%4s] %s\n", et_test_get_status_mnemonic(last_ent->status), last_ent->test->name); if (last_ent->ext_status) { printf("Test says:\n%s\n", last_ent->ext_status); } else { printf("No extended status.\n"); } putchar('\n'); printf("Press A to continue.\n"); printf("Press B to finish testing.\n"); printf("Press HOME/START to exit.\n"); return 0; } static int crit_test_fail_tick(const struct et_state *state, void *data, struct et_next_state *next_state) { struct et_test_plan *plan = *(void **)data; u32 wpad_pressed = WPAD_ButtonsDown(0); u32 pad_pressed = PAD_ButtonsDown(0); if (wpad_pressed & WPAD_BUTTON_A || pad_pressed & PAD_BUTTON_A) { next_state->state = et_state_begin_testing; /* also works to resume testing */ next_state->init_data.p = plan; /* test plan */ return 1; } if (wpad_pressed & WPAD_BUTTON_B || pad_pressed & PAD_BUTTON_B) { next_state->state = plan->next_state; next_state->init_data = plan->next_state_init; return 1; } if (wpad_pressed & WPAD_BUTTON_HOME || pad_pressed & PAD_BUTTON_START) { printf("Goodbye!\n"); exit(0); } return 0; } static void crit_test_fail_cleanup(const struct et_state *state, void *data) { /* do nothing */ } static const struct et_state state_crit_test_fail = { .private_size = sizeof(struct et_test_plan *), .init_f = crit_test_fail_init, .tick_f = crit_test_fail_tick, .cleanup_f = crit_test_fail_cleanup }; int test_next(struct et_test_plan *plan, struct et_next_state *next_state) { /* NOTE: this function doesn't increment current_test because the incremented value * would be visible in the cleanup function of the test state. */ /* NOTE: this condition will not trigger for states that leak their TEST_PRIMORDIAL status. * I don't care about this situation so much, since that should only happen for bugged tests. */ const struct et_test_plan_entry *ent = test_current(plan); switch (TEST_STATUS_GET_TYPE(ent->status)) { case TEST_SERROR: case TEST_SFAIL: if (ent->test->flags & TEST_PAUSE_ON_ERROR) { next_state->state = &state_crit_test_fail; next_state->init_data.p = plan; return 1; } break; default: /* do nothing */ (void)0; } if (plan->current_test == plan->num_tests - 1) { /* this is the last state... we're done! */ next_state->state = plan->next_state; next_state->init_data = plan->next_state_init; } else { next_state->state = &plan->tests[plan->current_test + 1].test->parent; next_state->init_data.p = plan; } return 1; /* always return 1: advance to next state */ } int test_state_init(const struct et_state *state, void *data, et_state_init_data init_data) { struct et_test_plan *plan = init_data.p; struct et_test_state_private_base *testdata = data; struct et_test_plan_entry *cur = test_current(plan); const struct et_test *test = (const struct et_test *)state; testdata->plan = plan; assert(cur->test == test); /* ensure correct test was loaded */ assert(TEST_STATUS_IS_CLEAN(cur->status)); if (test->test_init_f) { return test->test_init_f(state, data, init_data); } return 0; } void test_state_cleanup(const struct et_state *state, void *data) { struct et_test_state_private_base *testdata = data; struct et_test_plan_entry *cur = test_current(testdata->plan); const struct et_test *test = (const struct et_test *)state; if (test->test_cleanup_f) { test->test_cleanup_f(state, data); } if (TEST_STATUS_IS_CLEAN(cur->status)) { /* test didn't set status :( */ test_plan_entry_set_error_static(cur, "test aborted"); cur->status = TEST_ERROR(TEST_EABORT); } printf("[%4s] %s\n", et_test_get_status_mnemonic(cur->status), test->name); ++testdata->plan->current_test; } static int begin_testing_init(const struct et_state *state, void *state_data, et_state_init_data init_data) { struct et_test_plan *test_plan = init_data.p; struct et_test_plan **pst_test_plan = state_data; *pst_test_plan = test_plan; return 0; } static int begin_testing_tick(const struct et_state *state, void *state_data, struct et_next_state *next_state) { struct et_test_plan **ptest_plan = state_data; struct et_test_plan *test_plan = *ptest_plan; assert(test_plan->current_test <= test_plan->num_tests); if (test_plan->current_test < test_plan->num_tests) { const struct et_test_plan_entry *next_test = test_plan->tests + test_plan->current_test; assert(test_plan->tests); assert(next_test->status == TEST_PRIMORDIAL); next_state->state = &next_test->test->parent; next_state->init_data.p = test_plan; } else { /* no tests to run in this plan */ next_state->state = test_plan->next_state; next_state->init_data = test_plan->next_state_init; } return 1; } static void begin_testing_cleanup(const struct et_state *state, void *state_data) { /* do nothing */ } static const struct et_state state_begin_testing_struct = { .private_size = sizeof(const struct et_test_plan *), .init_f = begin_testing_init, .tick_f = begin_testing_tick, .cleanup_f = begin_testing_cleanup }; const struct et_state *et_state_begin_testing = &state_begin_testing_struct; static int summ_test_plan_init(const struct et_state *state, void *state_data, et_state_init_data init_data) { struct et_test_plan *ptest_plan = (*(struct et_test_plan **)state_data = init_data.p); size_t passed = 0; size_t failed = 0; size_t errored = 0; size_t skipped = 0; size_t unknown = 0; for (size_t i = 0; i < ptest_plan->num_tests; ++i) { switch (TEST_STATUS_GET_TYPE(ptest_plan->tests[i].status)) { case TEST_SPASS: ++passed; break; case TEST_SFAIL: ++failed; break; case TEST_SERROR: ++errored; break; case TEST_SSKIPPED: ++skipped; break; default: ++unknown; break; } } printf("\nTesting complete.\nPASS: %zu, FAIL: %zu, ERROR: %zu, SKIP: %zu, UNK: %zu\n\n", passed, failed, errored, skipped, unknown); puts("Press A to test again. Press HOME/START to exit."); return 0; } static int summ_test_plan_tick(const struct et_state *state, void *state_data, struct et_next_state *next_state) { u32 wpad_buttons = WPAD_ButtonsDown(0); u32 pad_buttons = PAD_ButtonsDown(0); if (wpad_buttons & WPAD_BUTTON_A || pad_buttons & PAD_BUTTON_A) { next_state->state = et_state_setup; next_state->init_data = et_init_data_null; return 1; } if (wpad_buttons & WPAD_BUTTON_HOME || pad_buttons & PAD_BUTTON_START) { puts("Goodbye!"); exit(0); } return 0; } static void summ_test_plan_cleanup(const struct et_state *state, void *state_data) { struct et_test_plan *ptest_plan = *(void **)state_data; for (size_t i = 0; i < ptest_plan->num_tests; ++i) { et_test_plan_entry_cleanup(ptest_plan->tests + i); } free(ptest_plan->tests); free(ptest_plan); } static const struct et_state summ_test_plan_struct = { .private_size = sizeof(struct et_test_plan *), .init_f = summ_test_plan_init, .tick_f = summ_test_plan_tick, .cleanup_f = summ_test_plan_cleanup }; const struct et_state *et_state_summarize_test_plan = &summ_test_plan_struct; #define COUNT_TESTS(_x) 1 #define NUM_TESTS FOREACH_TEST(COUNT_TESTS, +) static const struct et_test *all_tests[NUM_TESTS + 1]; static bool tests_array_initialized = false; const struct et_test *const *et_get_tests(size_t *ntests) { size_t idx = 0; if (ntests) *ntests = NUM_TESTS; if (tests_array_initialized) return all_tests; #define A(_name) all_tests[idx++] = TEST_SYM(_name) FOREACH_TEST(A, ;); #undef A all_tests[idx] = NULL; tests_array_initialized = true; return all_tests; } static const char *error_oom = "(failed to allocate error message)"; static const char *error_bad_fmt = "(bad extended error format)"; void et_test_plan_entry_cleanup(struct et_test_plan_entry *entry) { if (entry->ext_status_free) { /* okay to cast away const here. we're careful about making sure this pointer is non-NULL iff this pointer * was malloc'd */ entry->ext_status_free((void *)entry->ext_status); } entry->ext_status = NULL; entry->ext_status_free = NULL; } void test_plan_entry_set_error(struct et_test_plan_entry *entry, const char *fmt, ...) { int res; va_list vp; char *dest; size_t dest_sz; et_test_plan_entry_cleanup(entry); va_start(vp, fmt); res = vsnprintf(NULL, 0, fmt, vp); va_end(vp); if (res < 0) { entry->ext_status = error_bad_fmt; entry->ext_status_free = NULL; return; } dest_sz = MIN((size_t)res + 1, (size_t)1024); dest = malloc(dest_sz); dest[0] = '\0'; if (!dest) { entry->ext_status = error_oom; entry->ext_status_free = NULL; return; } va_start(vp, fmt); res = vsnprintf(dest, dest_sz, fmt, vp); va_end(vp); dest[dest_sz - 1] = '\0'; assert(res >= 0); entry->ext_status = dest; entry->ext_status_free = free; } void test_plan_entry_set_error_static(struct et_test_plan_entry *entry, const char *str) { et_test_plan_entry_cleanup(entry); entry->ext_status = str; entry->ext_status_free = NULL; } const char *et_test_get_status_mnemonic(et_test_status status) { switch (TEST_STATUS_GET_TYPE(status)) { case TEST_SPASS: return "PASS"; case TEST_SFAIL: return "FAIL"; case TEST_SSKIPPED: if (TEST_STATUS_IS_CLEAN(status)) { return "SKI!"; } else { return "SKIP"; } case TEST_SERROR: switch (TEST_STATUS_GET_ERROR(status)) { case TEST_ENONE: return "ERR0"; case TEST_EABORT: return "ERRA"; default: return "ERRP"; } default: return "UNKS"; } }