#include #include #include #include #include #include #include #define TEST_INTERNAL #include "test.h" #include "term.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; struct summ_test_plan_private { struct et_test_plan *plan; size_t selected_test; }; #define SUMM_MENU_ROWS 3 static void summ_test_print_menu(struct summ_test_plan_private *priv) { const struct et_test *test = priv->plan->tests[priv->selected_test].test; fputs(TERM_CUR_UP(SUMM_MENU_ROWS), stdout); puts(TERM_CLEARLINE "Press A to test again. Press HOME/START to exit."); puts(TERM_CLEARLINE "Press +/X to show output for [DL/DR]:"); printf(TERM_CLEARLINE "%s\n", test->name); } static int summ_test_plan_init(const struct et_state *state, void *state_data, et_state_init_data init_data) { struct summ_test_plan_private *priv = state_data; struct et_test_plan *ptest_plan = init_data.p; size_t passed = 0; size_t failed = 0; size_t errored = 0; size_t skipped = 0; size_t unknown = 0; priv->plan = ptest_plan; priv->selected_test = 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); for (int i = 0; i < SUMM_MENU_ROWS; ++i) putchar('\n'); summ_test_print_menu(priv); return 0; } static int summ_test_plan_tick(const struct et_state *state, void *state_data, struct et_next_state *next_state) { struct summ_test_plan_private *priv = state_data; 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); } if (wpad_buttons & WPAD_BUTTON_LEFT || pad_buttons & PAD_BUTTON_LEFT) { if (priv->selected_test > 0) { --priv->selected_test; } else { priv->selected_test = priv->plan->num_tests - 1; } summ_test_print_menu(priv); } if (wpad_buttons & WPAD_BUTTON_RIGHT || pad_buttons & PAD_BUTTON_RIGHT) { if (priv->selected_test < priv->plan->num_tests - 1) { ++priv->selected_test; } else { priv->selected_test = 0; } summ_test_print_menu(priv); } if (wpad_buttons & WPAD_BUTTON_1 || pad_buttons & PAD_BUTTON_X) { struct et_test_plan_entry *entry = priv->plan->tests + priv->selected_test; printf("\nExtended status for %s:\n", entry->test->name); if (entry->ext_status) { puts(entry->ext_status); } else { puts("(no status)"); } for (int i = 0; i < SUMM_MENU_ROWS + 1; ++i) putchar('\n'); summ_test_print_menu(priv); } return 0; } static void summ_test_plan_cleanup(const struct et_state *state, void *state_data) { struct summ_test_plan_private *priv = state_data; struct et_test_plan *ptest_plan = priv->plan; 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 summ_test_plan_private), .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"; } } et_test_status test_exi_readwrite(struct et_test_plan *plan, void *data, size_t data_sz, et_test_error base) { struct et_test_plan_entry *cur = test_current(plan); int chan = plan->exi_channel; et_test_status status = TEST_PASS; s32 exi_res; if ((exi_res = EXI_Lock(chan, EXI_DEVICE_0, NULL)) <= 0) { status = TEST_ERROR(base); test_plan_entry_set_error(cur, "failed to lock EXI channel: %" PRId32, exi_res); goto fail_lock; } if ((exi_res = EXI_Select(chan, EXI_DEVICE_0, EXI_SPEED32MHZ)) <= 0) { status = TEST_ERROR(base + 1); test_plan_entry_set_error(cur, "failed to select EXI channel: %" PRId32, exi_res); goto fail_select; } if ((exi_res = EXI_Imm(chan, data, data_sz, EXI_READWRITE, NULL)) <= 0) { status = TEST_ERROR(base + 2); test_plan_entry_set_error(cur, "failed to send EXI data: %" PRId32, exi_res); goto fail_data; } if ((exi_res = EXI_Sync(chan)) <= 0) { status = TEST_ERROR(base + 3); test_plan_entry_set_error(cur, "failed to complete transfer: %" PRId32, exi_res); goto fail_data; } fail_data: EXI_Deselect(chan); fail_select: EXI_Unlock(chan); fail_lock: return status; }