summaryrefslogtreecommitdiffstats
path: root/source
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2026-06-27 22:53:26 -0500
committerLibravatar bigfoot547 <[email protected]>2026-06-27 22:53:26 -0500
commit40724faccacf055cd9f9bba4615b9f930be9b76b (patch)
tree8df1806ef766f14a935e98ad1a04e47714f5f858 /source
initial commit
Diffstat (limited to 'source')
-rw-r--r--source/macros.h12
-rw-r--r--source/state.h57
-rw-r--r--source/states/menu.c96
-rw-r--r--source/template.c303
-rw-r--r--source/term.h13
-rw-r--r--source/test-impl.inc.h49
-rw-r--r--source/test.c354
-rw-r--r--source/test.h120
-rw-r--r--source/tests/basic.c66
9 files changed, 1070 insertions, 0 deletions
diff --git a/source/macros.h b/source/macros.h
new file mode 100644
index 0000000..d2231ff
--- /dev/null
+++ b/source/macros.h
@@ -0,0 +1,12 @@
+#ifndef EXITEST_MACROS_H_INCLUDED
+#define EXITEST_MACROS_H_INCLUDED
+
+#define STR(_s) STR2(_s)
+#define STR2(_s) #_s
+
+#define PASTE(_x, _y) PASTE2(_x, _y)
+#define PASTE2(_x, _y) _x ## _y
+
+#define COMMA ,
+
+#endif /* include guard */
diff --git a/source/state.h b/source/state.h
new file mode 100644
index 0000000..2f70527
--- /dev/null
+++ b/source/state.h
@@ -0,0 +1,57 @@
+#ifndef EXITEST_STATE_H_INCLUDED
+#define EXITEST_STATE_H_INCLUDED
+
+#include <stdint.h>
+#include <stddef.h>
+
+struct et_state;
+
+typedef union {
+ uintptr_t i;
+ void *p;
+} et_state_init_data;
+
+extern const et_state_init_data et_init_data_null;
+
+struct et_next_state {
+ const struct et_state *state;
+ et_state_init_data init_data;
+};
+
+/* Sets up the state. The second argument (void *) should point to at least state->private_size bytes
+ * of data. The state assumes responsibility for clearing/initializing this buffer before use.
+ *
+ * Returns < 0 for error, >= 0 for success.
+ * This function conceptually takes ownership of init_data. The implications of this depend
+ * on what init_data actually contains -- sometimes it's globally shared data.
+ *
+ * State authors: Free any data filled into init_data regardless of error states.
+ * The caller will not touch init_data again. */
+typedef int (et_state_init_f)(const struct et_state *, void *, et_state_init_data);
+
+/* Ticks the state.
+ *
+ * Returns < 0 for error (still call cleanup or exit(...)), 0 for "continue", >0 for "advance state".
+ * If a state requests to advance to the next state, the info will be in *next_state (which should be non-NULL).
+ * Note that next_state->init_data may have malloc()'d data which must be handed off by passing to the init function
+ * of the appropriate state. */
+typedef int (et_state_tick_f)(const struct et_state *, void *, struct et_next_state *);
+typedef void (et_state_cleanup_f)(const struct et_state *, void *);
+
+struct et_state {
+ et_state_init_f *init_f;
+ et_state_tick_f *tick_f;
+ et_state_cleanup_f *cleanup_f;
+ size_t private_size;
+};
+
+/* convenience macros because these existed as functions at one point
+ * (note: state expression is evaluated twice. make sure it doesn't have side effects) */
+
+#define et_state_init(_st, _data, _init_data) ((_st)->init_f(_st, _data, _init_data))
+#define et_state_tick(_st, _data, _next) ((_st)->tick_f(_st, _data, _next))
+#define et_state_cleanup(_st, _data) ((_st)->cleanup_f(_st, _data))
+
+extern const struct et_state *et_state_setup;
+
+#endif /* include guard */
diff --git a/source/states/menu.c b/source/states/menu.c
new file mode 100644
index 0000000..15e0b3c
--- /dev/null
+++ b/source/states/menu.c
@@ -0,0 +1,96 @@
+#include <stdio.h>
+#include <gccore.h>
+#include <wiiuse/wpad.h>
+#include <stdlib.h>
+
+#define EXITEST_STATE_INTERNAL
+#include "../state.h"
+#include "../term.h"
+#include "../test.h"
+
+static void print_setup_menu(int selected_slot) {
+ fputs(TERM_CUR_UP(2), stdout);
+ printf(TERM_CLEARLINE "- [DL/DR] EXI Slot %c\n", selected_slot == 0 ? 'A' : 'B');
+ printf(TERM_CLEARLINE "Press A to begin testing.\n");
+}
+
+static int st_setup_init(const struct et_state *state, void *state_data, et_state_init_data init_data) {
+ u32 *setup_data = state_data;
+
+ *setup_data = 0;
+
+ printf("\nTest setup menu:\n\n\n\n");
+ print_setup_menu(*setup_data);
+
+ return 0;
+}
+
+static int begin_testing(u32 menu_state, struct et_next_state *next_state) {
+ struct et_test_plan *plan = calloc(1, sizeof(struct et_test_plan));
+ if (!plan) {
+ puts("Failed to allocate new test plan!");
+ return -1;
+ }
+
+ size_t num_tests;
+ const struct et_test *const *all_tests = et_get_tests(&num_tests);
+
+ struct et_test_plan_entry *plan_entries = calloc(num_tests, sizeof(struct et_test_plan_entry));
+ if (!plan_entries) {
+ puts("Failed to allocate new test plan entries!");
+ free(plan);
+ return -1;
+ }
+
+ for (size_t i = 0; i < num_tests; ++i) {
+ plan_entries[i].test = all_tests[i];
+ plan_entries[i].status = TEST_PRIMORDIAL;
+ plan_entries[i].ext_status = NULL;
+ plan_entries[i].ext_status_free = NULL;
+ }
+
+ plan->exi_channel = (int)menu_state;
+ plan->next_state = et_state_summarize_test_plan;
+ plan->next_state_init.p = plan;
+ plan->num_tests = num_tests;
+ plan->current_test = 0;
+ plan->tests = plan_entries;
+
+ next_state->state = et_state_begin_testing;
+ next_state->init_data.p = plan;
+
+ return 1;
+}
+
+static int st_setup_tick(const struct et_state *state, void *state_data, struct et_next_state *next_state) {
+ u32 *setup_data = state_data;
+ u32 wpad_pressed = WPAD_ButtonsDown(0);
+ u32 pad_pressed = PAD_ButtonsDown(0);
+
+ if (wpad_pressed & WPAD_BUTTON_LEFT || wpad_pressed & WPAD_BUTTON_RIGHT || pad_pressed & PAD_BUTTON_LEFT || pad_pressed & PAD_BUTTON_RIGHT) {
+ *setup_data = !*setup_data;
+ print_setup_menu(*setup_data);
+ }
+
+ if (wpad_pressed & WPAD_BUTTON_A || pad_pressed & PAD_BUTTON_A) {
+ /* set up test plan */
+ return begin_testing(*setup_data, next_state);
+ }
+
+ return 0;
+}
+
+static void st_setup_cleanup(const struct et_state *state, void *state_data) {
+ /* do nothing */
+}
+
+const et_state_init_data et_init_data_null = { .p = NULL };
+
+static struct et_state et__state_setup = {
+ .init_f = &st_setup_init,
+ .tick_f = &st_setup_tick,
+ .cleanup_f = &st_setup_cleanup,
+ .private_size = sizeof(u32)
+};
+
+const struct et_state *et_state_setup = &et__state_setup;
diff --git a/source/template.c b/source/template.c
new file mode 100644
index 0000000..8665774
--- /dev/null
+++ b/source/template.c
@@ -0,0 +1,303 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <gccore.h>
+#include <wiiuse/wpad.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include "term.h"
+#include "state.h"
+
+static void *xfb = NULL;
+static GXRModeObj *rmode = NULL;
+
+#define TARGET_IOS 58
+
+static void initialize(int again) {
+ VIDEO_Init();
+
+ rmode = VIDEO_GetPreferredMode(NULL);
+ xfb = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
+
+ console_init(xfb, 20, 20, rmode->fbWidth - 20, rmode->xfbHeight - 20, rmode->fbWidth * VI_DISPLAY_PIX_SZ);
+
+ VIDEO_Configure(rmode);
+ VIDEO_SetNextFramebuffer(xfb);
+ VIDEO_ClearFrameBuffer(rmode, xfb, COLOR_BLACK);
+ VIDEO_SetBlack(FALSE);
+ VIDEO_Flush();
+ VIDEO_WaitVSync();
+ if (rmode->viTVMode & VI_NON_INTERLACE) {
+ VIDEO_WaitVSync();
+ }
+
+ fputs(TERM_CLEAR TERM_CUR_POS(2, 5), stdout);
+ printf("USB Gecko (newnewexi) test tool by figboot\n\n");
+
+ s32 iosver = IOS_GetVersion();
+ s32 iosrev = IOS_GetRevision();
+ printf("You're running IOS%" PRId32 " rev %" PRId32 ".\n", iosver, iosrev);
+
+ if (iosver != TARGET_IOS) {
+ if (again) {
+ printf(TERM_CSI("93m") "Failed to set up IOS" STR(TARGET_IOS) "! Bailing out.\n");
+ exit(1);
+ }
+
+ printf("Trying to load IOS" STR(TARGET_IOS) "...\n");
+
+ s32 res;
+ if ((res = IOS_ReloadIOS(TARGET_IOS)) < 0) {
+ printf(TERM_CSI("93m") "Failed to set up IOS" STR(TARGET_IOS) "! IOS_ReloadIOS returned %" PRId32 "\n", res);
+ exit(1);
+ }
+
+ free(MEM_K1_TO_K0(xfb));
+ initialize(1);
+ }
+}
+
+static void __attribute__((noreturn)) fatal_error(void) {
+ printf("The program will now abort due to the fatal error.\n");
+ exit(1);
+}
+
+int main(int argc, char **argv)
+{
+ void *state_data = NULL;
+ size_t state_data_size = 0;
+ struct et_next_state next;
+ const struct et_state *cur_state = NULL;
+ int stateres = 0;
+
+ u32 wpad_pressed;
+ u32 pad_pressed;
+
+ initialize(0);
+
+ WPAD_Init();
+ PAD_Init();
+
+ printf("Done initializing.\n");
+
+ /* Set up initial state */
+ cur_state = et_state_setup;
+
+ if (cur_state->private_size > 0) {
+ state_data = malloc(cur_state->private_size);
+ state_data_size = cur_state->private_size;
+ if (!state_data) {
+ printf("Failed to allocate private space for setup state %p!", cur_state);
+ fatal_error();
+ }
+ }
+
+ if ((stateres = et_state_init(cur_state, state_data, et_init_data_null)) < 0) {
+ printf("Failed to init setup state %p: %d\n", cur_state, stateres);
+ fatal_error();
+ }
+
+ while (SYS_MainLoop()) {
+ WPAD_ScanPads();
+ PAD_ScanPads();
+
+ wpad_pressed = WPAD_ButtonsDown(0);
+ pad_pressed = PAD_ButtonsDown(0);
+
+ if (wpad_pressed & WPAD_BUTTON_HOME || pad_pressed & PAD_BUTTON_START) {
+ printf("Exit button pressed. Goodbye!\n");
+ exit(0);
+ }
+
+ stateres = et_state_tick(cur_state, state_data, &next);
+ if (stateres < 0) {
+ printf("Failed to tick state %p: %d\n", cur_state, stateres);
+ fatal_error();
+ }
+
+ if (stateres > 0) {
+ /* tear down the old state first... */
+ et_state_cleanup(cur_state, state_data);
+
+ cur_state = next.state;
+
+ if (state_data_size < cur_state->private_size) {
+ void *new_data = realloc(state_data, cur_state->private_size);
+ if (!new_data) {
+ printf("Failed to allocate %zu bytes for new state %p\n", cur_state->private_size, cur_state);
+ fatal_error();
+ }
+
+ state_data = new_data;
+ state_data_size = cur_state->private_size;
+ }
+
+ if ((stateres = et_state_init(cur_state, state_data, next.init_data)) < 0) {
+ printf("Failed to init new state %p: %d\n", cur_state, stateres);
+ fatal_error();
+ }
+ }
+
+ VIDEO_WaitVSync();
+ }
+
+ return 0;
+}
+
+#if 0
+
+//---------------------------------------------------------------------------------
+int main(int argc, char **argv) {
+//---------------------------------------------------------------------------------
+
+ // Initialise the video system
+ VIDEO_Init();
+
+ // This function initialises the attached controllers
+ WPAD_Init();
+
+ // Obtain the preferred video mode from the system
+ // This will correspond to the settings in the Wii menu
+ rmode = VIDEO_GetPreferredMode(NULL);
+
+ // Allocate memory for the display in the uncached region
+ xfb = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
+
+ // Initialise the console, required for printf
+ console_init(xfb,20,20,rmode->fbWidth,rmode->xfbHeight,rmode->fbWidth*VI_DISPLAY_PIX_SZ);
+
+ // Set up the video registers with the chosen mode
+ VIDEO_Configure(rmode);
+
+ // Tell the video hardware where our display memory is
+ VIDEO_SetNextFramebuffer(xfb);
+
+ // Make the display visible
+ VIDEO_SetBlack(false);
+
+ // Flush the video register changes to the hardware
+ VIDEO_Flush();
+
+ // Wait for Video setup to complete
+ VIDEO_WaitVSync();
+ if(rmode->viTVMode&VI_NON_INTERLACE) VIDEO_WaitVSync();
+
+
+ // The console understands VT terminal escape codes
+ // This positions the cursor on row 2, column 0
+ // we can use variables for this with format codes too
+ // e.g. printf ("\x1b[%d;%dH", row, column );
+ printf("\x1b[2;0H");
+
+
+ printf("Hello World!\n");
+
+#define UGCHAN EXI_CHANNEL_1 /* slot B */
+#define UGDEV EXI_DEVICE_0 /* memory card */
+
+ if (EXI_Lock(UGCHAN, UGDEV, NULL) == 1) {
+ EXI_Select(UGCHAN, UGDEV, EXI_SPEED32MHZ);
+ uint32_t cmd = 0x90000000;
+ int res = 0;
+ if (EXI_Imm(UGCHAN, &cmd, sizeof(cmd), EXI_READWRITE, NULL) == 0) res |= 0x01;
+ if (EXI_Sync(UGCHAN) == 0) res |= 0x02;
+
+ if (res == 0) {
+ printf("Got: 0x%08x\n", cmd);
+ if (cmd != 0x04700000) {
+ printf("This is probably not a USB gecko...\n");
+ }
+ } else {
+ printf("EXI read/write error :( %#04x\n", res);
+ }
+
+ EXI_Deselect(UGCHAN);
+ EXI_Unlock(UGCHAN);
+ } else {
+ printf("Failed to lock EXI channel %d %d\n", UGCHAN, UGDEV);
+ goto waitexit;
+ }
+
+ for (int i = 0; i < 10; ++i) {
+ if (EXI_Lock(UGCHAN, UGDEV, NULL) == 0) {
+ printf("Failed to lock EXI for LED\n");
+ break;
+ }
+
+ int res = 0;
+
+ if (EXI_Select(UGCHAN, UGDEV, EXI_SPEED32MHZ) == 0) res |= 0x01;
+ uint32_t cmd = (i & 1) ? 0x80000000 : 0x70000000;
+ if (EXI_Imm(UGCHAN, &cmd, sizeof(cmd), EXI_WRITE, NULL) == 0) res |= 0x02;
+ if (EXI_Sync(UGCHAN) == 0) res |= 0x04;
+ if (EXI_Deselect(UGCHAN) == 0) res |= 0x08;
+
+ if (EXI_Unlock(UGCHAN) == 0) res |= 0x10;
+
+ printf("Error for LED: 0x%02x\n", res);
+
+ for (int j = 0; j < 60; ++j) {
+ VIDEO_WaitVSync();
+ }
+ }
+
+
+ for (int j = 0; j < 10; ++j) {
+ if (EXI_Lock(UGCHAN, UGDEV, NULL) == 1) {
+ printf("Sending a zillion bytes to your so-called \"usb gecko\"...\n");
+
+ for (int i = 0; i < 4096; ++i) {
+ EXI_Select(UGCHAN, UGDEV, EXI_SPEED32MHZ);
+
+ uint16_t cmd = 0xB000 | (uint16_t)(((i & 63) + 32) << 4);
+ if (EXI_Imm(UGCHAN, &cmd, sizeof(cmd), EXI_READWRITE, NULL) == 0) {
+ printf("Failed when writing %d\n", i);
+ goto error;
+ }
+
+ if (EXI_Sync(UGCHAN) == 0) {
+ printf("Failed sync when writing %d\n", i);
+ goto error;
+ }
+
+ if ((cmd & 0x0400) == 0) {
+ printf("Out of FIFO space at %d\n", i);
+ goto error;
+ }
+
+ EXI_Deselect(UGCHAN);
+ continue;
+error:
+ EXI_Deselect(UGCHAN);
+ break;
+ }
+
+ EXI_Unlock(UGCHAN);
+ } else {
+ printf("Failed to lock EXI channel %d %d\n", UGCHAN, UGDEV);
+ goto waitexit;
+ }
+ }
+
+waitexit:
+ printf("That's all! Press HOME to exit.\n");
+
+ while(SYS_MainLoop()) {
+
+ // Call WPAD_ScanPads each loop, this reads the latest controller states
+ WPAD_ScanPads();
+
+ // WPAD_ButtonsDown tells us which buttons were pressed in this loop
+ // this is a "one shot" state which will not fire again until the button has been released
+ u32 pressed = WPAD_ButtonsDown(0);
+
+ // We return to the launcher application via exit
+ if ( pressed & WPAD_BUTTON_HOME ) exit(0);
+
+ // Wait for the next frame
+ VIDEO_WaitVSync();
+ }
+
+ return 0;
+}
+#endif
diff --git a/source/term.h b/source/term.h
new file mode 100644
index 0000000..cc65615
--- /dev/null
+++ b/source/term.h
@@ -0,0 +1,13 @@
+#ifndef EXITEST_TERM_H_INCLUDED
+#define EXITEST_TERM_H_INCLUDED
+
+#include "macros.h"
+
+#define TERM_ESC "\x1b"
+#define TERM_CSI(_s) TERM_ESC "[" _s
+#define TERM_CLEAR TERM_CSI("2J")
+#define TERM_CLEARLINE TERM_CSI("2K")
+#define TERM_CUR_POS(_row, _col) TERM_CSI(STR(_row) ";" STR(_col) "f")
+#define TERM_CUR_UP(_rows) TERM_CSI(STR(_rows) "A")
+
+#endif /* include guard */
diff --git a/source/test-impl.inc.h b/source/test-impl.inc.h
new file mode 100644
index 0000000..2ecb7a6
--- /dev/null
+++ b/source/test-impl.inc.h
@@ -0,0 +1,49 @@
+#ifndef ET__ALLOW_TEST_IMPL_INC
+#error "Do not include this file directly! Include test.h instead."
+#endif
+
+#undef ET__ALLOW_TEST_IMPL_INC
+
+/* This file will be included in .c files which implement tests. */
+#ifdef _CLANGD
+#include <gccore.h>
+#include "test.h"
+/* include stuff this file uses here to make clangd happy (will not be visible to test.h) */
+#endif
+
+struct et_test_state_private_base {
+ struct et_test_plan *plan;
+};
+
+/* function prototypes for test-internal stuff */
+int test_next(struct et_test_plan *plan, struct et_next_state *next_state);
+
+int test_state_init(const struct et_state *state, void *data, et_state_init_data init_data);
+void test_state_cleanup(const struct et_state *state, void *data);
+
+void test_plan_entry_set_error(struct et_test_plan_entry *entry, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+void test_plan_entry_set_error_static(struct et_test_plan_entry *entry, const char *str);
+
+/* Expands to a struct initializer for a struct et_test */
+#define TEST_STRUCT_INIT_FULL(_namestr, _flags, _init_f, _tick_f, _cleanup_f, _private_type) \
+{ \
+ .parent = { \
+ .init_f = test_state_init, \
+ .tick_f = _tick_f, \
+ .cleanup_f = test_state_cleanup, \
+ .private_size = sizeof(_private_type), \
+ }, \
+ .name = _namestr, \
+ .flags = _flags \
+}
+
+#define TEST__STRUCT_NAME(_name) PASTE(TEST_SYM(_name), __struct)
+#define TEST__STRUCT_DEF(_name, _ini) static const struct et_test TEST__STRUCT_NAME(_name) = _ini
+#define TEST__STRUCT_SANITY_CHECK(_ty) static_assert(sizeof(_ty) >= sizeof(struct et_test_state_private_base), "private data type too small (does it include base?)")
+#define TEST__SYM_DEF(_name) const struct et_test *const TEST_SYM(_name) = &TEST__STRUCT_NAME(_name)
+#define TEST_DEF(_name, _namestr, _flags, _init_f, _tick_f, _cleanup_f, _private_type) \
+ TEST__STRUCT_SANITY_CHECK(_private_type); \
+ TEST__STRUCT_DEF(_name, TEST_STRUCT_INIT_FULL(_namestr, _flags, _init_f, _tick_f, _cleanup_f, _private_type)); \
+ TEST__SYM_DEF(_name)
+
+#define test_current(_plan) ((_plan)->tests + (_plan)->current_test)
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";
+ }
+}
diff --git a/source/test.h b/source/test.h
new file mode 100644
index 0000000..d45c2fe
--- /dev/null
+++ b/source/test.h
@@ -0,0 +1,120 @@
+#ifndef EXITEST_TEST_H_INCLUDED
+#define EXITEST_TEST_H_INCLUDED
+
+#include <gccore.h>
+#include <assert.h>
+#include "state.h"
+#include "macros.h"
+
+typedef enum {
+ TEST_INTERACTIVE = 0x01u,
+ TEST_PAUSE_ON_ERROR = 0x02u /* for "essential" tests that should pass */
+} et_test_flags;
+
+struct et_test;
+struct et_test_plan;
+
+struct et_test {
+ struct et_state parent;
+ const char *name;
+ et_test_flags flags;
+
+ et_state_init_f *test_init_f;
+ et_state_cleanup_f *test_cleanup_f;
+};
+
+/* 0000eeee eeeeeeee eeeeeeee x00000ss */
+/* e = error code bits,
+ * x = 1 if this test is still in the "clean" (not executed) state
+ * ss = status type (pass/fail/etc.) */
+typedef u32 et_test_status;
+
+typedef enum {
+ TEST_ENONE = (u32)0,
+
+ /* test was aborted (i.e., requested to go to next test without setting a status code) */
+ TEST_EABORT,
+
+ /* minimum private test error (define test-private error codes as TEST_EPRIV_BASE+n) */
+ TEST_EPRIV_BASE = 32,
+
+ /* largest error code allowed */
+ TEST_EMAX = ((u32)1 << 20) - 1
+} et_test_error;
+
+typedef enum {
+ TEST_SSKIPPED = 0u,
+ TEST_SERROR,
+ TEST_SFAIL,
+ TEST_SPASS,
+
+ TEST_SMAX = ((u32)1 << 2) - 1
+} et_test_status_type;
+
+#define TEST_STATUS_GET_ERROR(_status) (et_test_error)(((u32)(_status) >> 8) & TEST_EMAX)
+#define TEST_STATUS_GET_TYPE(_status) (et_test_status_type)((u32)(_status) & TEST_SMAX)
+#define TEST_STATUS_IS_CLEAN(_status) (!!((u32)(_status) & 0x80u))
+
+#define TEST_MAKE_STATUS(_type, _error, _clean) (et_test_status) \
+ (((u32)(_type) & TEST_SMAX) | \
+ (((u32)(_clean) & 1) << 7) | \
+ (((u32)(_error) & TEST_EMAX) << 8))
+
+#define TEST_SKIPPED TEST_MAKE_STATUS(TEST_SSKIPPED, TEST_ENONE, 0)
+#define TEST_FAIL TEST_MAKE_STATUS(TEST_SFAIL, TEST_ENONE, 0)
+#define TEST_PASS TEST_MAKE_STATUS(TEST_SPASS, TEST_ENONE, 0)
+#define TEST_ERROR(_e) TEST_MAKE_STATUS(TEST_SERROR, _e, 0)
+
+#define TEST_PRIMORDIAL TEST_MAKE_STATUS(TEST_SSKIPPED, TEST_ENONE, 1)
+
+/* Expected setup date: pointer to test plan */
+extern const struct et_state *et_state_begin_testing;
+extern const struct et_state *et_state_summarize_test_plan;
+
+const char *et_test_get_status_mnemonic(et_test_status status);
+
+struct et_test_plan_entry {
+ const struct et_test *test;
+ et_test_status status;
+ const char *ext_status;
+ void (*ext_status_free)(void *);
+};
+
+/* a properly initialized test plan will have all the tests' statuses set to TEST_PRIMORDIAL */
+struct et_test_plan {
+ int exi_channel;
+
+ /* this state will be set after testing is complete.
+ * NOTE: next_state_init should somehow point to this test plan,
+ * since it's probably dynamically allocated and will be leaked if not
+ * freed. */
+ const struct et_state *next_state;
+ et_state_init_data next_state_init;
+
+ size_t num_tests;
+ size_t current_test;
+ struct et_test_plan_entry *tests;
+};
+
+void et_test_plan_entry_cleanup(struct et_test_plan_entry *entry);
+
+#define FOREACH_TEST(O, _sep) \
+ O(check_id)
+
+#define TEST_SYM(_name) PASTE(et_test_, _name)
+#define DECL_TEST(_name) extern const struct et_test *const TEST_SYM(_name)
+
+FOREACH_TEST(DECL_TEST, ;);
+#undef DECL_TEST
+
+const struct et_test *const *et_get_tests(size_t *ntests);
+
+#if defined(ET_TEST_IMPL) || defined(TEST_INTERNAL)
+#define ET__ALLOW_TEST_IMPL_INC
+#include "test-impl.inc.h"
+#else
+#undef FOREACH_TEST
+#undef TEST_SYM
+#endif
+
+#endif /* include guard */
diff --git a/source/tests/basic.c b/source/tests/basic.c
new file mode 100644
index 0000000..8645b19
--- /dev/null
+++ b/source/tests/basic.c
@@ -0,0 +1,66 @@
+#define ET_TEST_IMPL
+#include "../test.h"
+#include <inttypes.h>
+
+et_test_status test_exi_readwrite(struct et_test_plan *plan, void *data, size_t data_sz, et_test_error base, struct et_next_state *next_state) {
+ 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:
+ cur->status = status;
+ return status;
+}
+
+static int check_id_tick(const struct et_state *state, void *data, struct et_next_state *next_state) {
+ struct et_test_state_private_base *priv = data;
+ struct et_test_plan *test_plan = priv->plan;
+ struct et_test_plan_entry *cur = test_current(test_plan);
+ et_test_status status = TEST_PASS;
+ u16 command = 0x9000;
+
+ if ((status = test_exi_readwrite(test_plan, &command, sizeof(command), TEST_EPRIV_BASE, next_state)) != TEST_PASS) {
+ return test_next(test_plan, next_state);
+ }
+
+ if (command != 0x0470) {
+ test_plan_entry_set_error(cur, "bad identifier from gecko: expected 0x0470, got 0x%04" PRIx16, command);
+ status = TEST_FAIL;
+ }
+
+ cur->status = TEST_PASS;
+ return test_next(test_plan, next_state);
+}
+
+TEST_DEF(check_id, "Identify Gecko (0x09)", TEST_PAUSE_ON_ERROR, NULL, check_id_tick, NULL, struct et_test_state_private_base);