summaryrefslogtreecommitdiffstats
path: root/source/test.c
diff options
context:
space:
mode:
Diffstat (limited to 'source/test.c')
-rw-r--r--source/test.c354
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";
+ }
+}