diff options
Diffstat (limited to 'source/test.c')
| -rw-r--r-- | source/test.c | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/source/test.c b/source/test.c new file mode 100644 index 0000000..2013e04 --- /dev/null +++ b/source/test.c @@ -0,0 +1,354 @@ +#include <assert.h> +#include <stdio.h> +#include <gccore.h> +#include <wiiuse/wpad.h> +#include <stdlib.h> +#include <stdarg.h> + +#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"; + } +} |
