summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.clangd13
-rw-r--r--.gitignore4
-rw-r--r--Makefile138
-rw-r--r--fire-pit/test.c49
-rw-r--r--fire-pit/test.h77
-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
14 files changed, 1351 insertions, 0 deletions
diff --git a/.clangd b/.clangd
new file mode 100644
index 0000000..ed00e13
--- /dev/null
+++ b/.clangd
@@ -0,0 +1,13 @@
+CompileFlags:
+ Add:
+ - --target=powerpc-eabi
+ - -mcpu=750
+ - -DGEKKO
+ - -DHW_RVL
+ - -D__PPC__
+ - -isystem
+ - /opt/devkitpro/devkitPPC/powerpc-eabi/include
+ - -isystem
+ - /opt/devkitpro/devkitPPC/lib/gcc/powerpc-eabi/16.1.0/include
+ - -D_CLANGD
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aaeb505
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+build/
+compile_commands.json
+*.elf
+*.dol
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f896f14
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,138 @@
+#---------------------------------------------------------------------------------
+# Clear the implicit built in rules
+#---------------------------------------------------------------------------------
+.SUFFIXES:
+#---------------------------------------------------------------------------------
+ifeq ($(strip $(DEVKITPPC)),)
+$(error "Please set DEVKITPPC in your environment. export DEVKITPPC=<path to>devkitPPC")
+endif
+
+include $(DEVKITPPC)/wii_rules
+
+#---------------------------------------------------------------------------------
+# TARGET is the name of the output
+# BUILD is the directory where object files & intermediate files will be placed
+# SOURCES is a list of directories containing source code
+# INCLUDES is a list of directories containing extra header files
+#---------------------------------------------------------------------------------
+TARGET := $(notdir $(CURDIR))
+BUILD := build
+SOURCES := source source/states source/tests
+DATA := data
+INCLUDES :=
+
+#---------------------------------------------------------------------------------
+# options for code generation
+#---------------------------------------------------------------------------------
+
+CFLAGS = -g -O2 -Wall $(MACHDEP) $(INCLUDE)
+CXXFLAGS = $(CFLAGS)
+
+LDFLAGS = -g $(MACHDEP) -Wl,-Map,$(notdir $@).map
+
+#---------------------------------------------------------------------------------
+# any extra libraries we wish to link with the project
+#---------------------------------------------------------------------------------
+LIBS := -lwiiuse -lbte -logc -lm
+
+#---------------------------------------------------------------------------------
+# list of directories containing libraries, this must be the top level containing
+# include and lib
+#---------------------------------------------------------------------------------
+LIBDIRS :=
+
+#---------------------------------------------------------------------------------
+# no real need to edit anything past this point unless you need to add additional
+# rules for different file extensions
+#---------------------------------------------------------------------------------
+ifneq ($(BUILD),$(notdir $(CURDIR)))
+#---------------------------------------------------------------------------------
+
+export OUTPUT := $(CURDIR)/$(TARGET)
+
+export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
+ $(foreach dir,$(DATA),$(CURDIR)/$(dir))
+
+export DEPSDIR := $(CURDIR)/$(BUILD)
+
+#---------------------------------------------------------------------------------
+# automatically build a list of object files for our project
+#---------------------------------------------------------------------------------
+CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
+CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
+sFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
+SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.S)))
+BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
+
+#---------------------------------------------------------------------------------
+# use CXX for linking C++ projects, CC for standard C
+#---------------------------------------------------------------------------------
+ifeq ($(strip $(CPPFILES)),)
+ export LD := $(CC)
+else
+ export LD := $(CXX)
+endif
+
+export OFILES_BIN := $(addsuffix .o,$(BINFILES))
+export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(sFILES:.s=.o) $(SFILES:.S=.o)
+export OFILES := $(OFILES_BIN) $(OFILES_SOURCES)
+
+export HFILES := $(addsuffix .h,$(subst .,_,$(BINFILES)))
+
+#---------------------------------------------------------------------------------
+# build a list of include paths
+#---------------------------------------------------------------------------------
+export INCLUDE := $(foreach dir,$(INCLUDES), -iquote $(CURDIR)/$(dir)) \
+ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \
+ -I$(CURDIR)/$(BUILD) \
+ -I$(LIBOGC_INC)
+
+#---------------------------------------------------------------------------------
+# build a list of library paths
+#---------------------------------------------------------------------------------
+export LIBPATHS := -L$(LIBOGC_LIB) $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
+
+export OUTPUT := $(CURDIR)/$(TARGET)
+.PHONY: $(BUILD) clean
+
+#---------------------------------------------------------------------------------
+$(BUILD):
+ @[ -d $@ ] || mkdir -p $@
+ @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
+
+#---------------------------------------------------------------------------------
+clean:
+ @echo clean ...
+ @rm -fr $(BUILD) $(OUTPUT).elf $(OUTPUT).dol
+
+#---------------------------------------------------------------------------------
+run:
+ wiiload $(TARGET).dol
+
+
+#---------------------------------------------------------------------------------
+else
+
+DEPENDS := $(OFILES:.o=.d)
+
+#---------------------------------------------------------------------------------
+# main targets
+#---------------------------------------------------------------------------------
+$(OUTPUT).dol: $(OUTPUT).elf
+$(OUTPUT).elf: $(OFILES)
+
+$(OFILES_SOURCES) : $(HFILES)
+
+#---------------------------------------------------------------------------------
+# This rule links in binary data with the .jpg extension
+#---------------------------------------------------------------------------------
+%.jpg.o %_jpg.h : %.jpg
+#---------------------------------------------------------------------------------
+ @echo $(notdir $<)
+ $(bin2o)
+
+-include $(DEPENDS)
+
+#---------------------------------------------------------------------------------
+endif
+#---------------------------------------------------------------------------------
diff --git a/fire-pit/test.c b/fire-pit/test.c
new file mode 100644
index 0000000..822db4f
--- /dev/null
+++ b/fire-pit/test.c
@@ -0,0 +1,49 @@
+#include "test.h"
+
+static int identify_setup(struct et_test *test, void *work, int exi_channel)
+{
+ int *pexi_channel = work;
+ *pexi_channel = exi_channel;
+ return 1;
+}
+
+static int identify_tick(struct et_test *test, void *work)
+{
+ int exi_channel = *((int *)work);
+
+ EXI_Lock(exi_channel, EXI_DEVICE_0, NULL);
+}
+
+static et_status identify_finish(struct et_test *test, void *work)
+{
+
+}
+
+static const char *identify_translate(struct et_test *test, char *out, size_t outbuf_sz, et_status status)
+{
+
+}
+
+#define DEF_TEST(_flags, _name, _work_t, _setup, _tick, _finish, ...) { \
+ .flags = _flags, \
+ .name = _name, \
+ .work_size = sizeof(_work_t), \
+ .setup_f = _setup, \
+ .tick_f = _tick, \
+ .finish_f = _finish, \
+ __VA_ARGS__ \
+}
+
+#define msg_table priv_data[0]
+#define TEST_MSGS(_msg_table) .msg_table = _msg_table
+
+#define TEST_TRANSLATE_DEFAULT .trans_f = &et_translate_default,
+#define TEST_TRANSLATE(_t) .trans_f = _t,
+
+const struct et_test et_tests[] = {
+ DEF_TEST(ET_TEST_ESSENTIAL, "Identify_Gecko", int, identify_setup, identify_tick, identify_finish, TEST_TRANSLATE(identify_translate)),
+};
+
+
+const size_t et_ntests = sizeof(et_tests) / sizeof(et_tests[0]);
+
diff --git a/fire-pit/test.h b/fire-pit/test.h
new file mode 100644
index 0000000..c907530
--- /dev/null
+++ b/fire-pit/test.h
@@ -0,0 +1,77 @@
+#ifndef EXITEST_TEST_H_INCLUDED
+#define EXITEST_TEST_H_INCLUDED
+
+#include <gccore.h>
+
+struct et_test;
+
+typedef u32 et_status;
+
+/* Test passed. */
+#define ET_STATUS_PASS (et_status)0x00
+
+/* Test failed. */
+#define ET_STATUS_FAIL (et_status)0x01
+
+/* Test skipped or not applicable. */
+#define ET_STATUS_SKIP (et_status)0x02
+
+/* Internal error during testing. */
+#define ET_STATUS_ERROR (et_status)0x03
+
+#define ET_STATUS_MASK (et_status)0x03
+
+#define ET_STATUS_GET_BASE(_status) ((et_status)(_status) & ~((1u << ET_STATUS_ERRCODE_SHIFT) - 1))
+
+#define ET_STATUS_ERRCODE_SHIFT 2u
+#define ET_STATUS_ERRCODE_MASK (((1u << ET_STATUS_PRIVATE_SHIFT) - 1) & ~ET_STATUS_MASK)
+#define ET_STATUS_MAKE_ERRCODE(_code) (((et_status)(_code) << ET_STATUS_ERRCODE_SHIFT) & ET_STATUS_ERRCODE_MASK)
+#define ET_STATUS_GET_ERRCODE(_status) (((et_status)(_status) & ET_STATUS_ERRCODE_MASK) >> ET_STATUS_ERRCODE_SHIFT)
+
+#define ET_STATUS_PRIVATE_SHIFT 8u
+#define ET_STATUS_GET_PRIVATE(_status) ((et_status)(_status) >> ET_STATUS_PRIVATE_SHIFT)
+#define ET_STATUS_MAKE_PRIVATE(_exp) ((et_status)(_exp) << ET_STATUS_PRIVATE_SHIFT)
+#define ET_STATUS_PRIVATE_MASK (ET_STATUS_MAKE_PRIVATE(~0u))
+
+typedef enum {
+ ET_TEST_INTERACTIVE = 0x0001u,
+ ET_TEST_ESSENTIAL = 0x0002u
+} et_test_flags;
+
+typedef enum {
+ ET_ERRCODE_NONE = ET_STATUS_MAKE_ERRCODE(0),
+ ET_ERRCODE_BAD_ALLOC = ET_STATUS_MAKE_ERRCODE(0x01u),
+
+ /* Error code is test-specific */
+ ET_ERRCODE_PRIVATE = ET_STATUS_MAKE_ERRCODE(~0u),
+} et_status_errcode;
+
+/* returns 1 to continue testing, 0 when test is complete (call finish for status) */
+typedef int (et_test_setup_proc)(struct et_test *test, void *work, int exi_channel);
+typedef int (et_test_tick_proc)(struct et_test *test, void *work);
+typedef et_status (et_test_finish_proc)(struct et_test *test, void *work);
+
+/* translates a status code into a readable message. The returned pointer is a static string, and may not have all the information
+ * as if a writeable buffer is passed into the function.
+ *
+ * Tests: do not write into out if outbuf_sz is too small. Try to keep the max message length within 256 bytes. */
+typedef const char *(et_test_translate_proc)(struct et_test *test, char *out, size_t outbuf_sz, et_status status);
+
+struct et_test {
+ et_test_flags flags;
+ const char *name;
+ size_t work_size;
+
+ et_test_setup_proc *setup_f;
+ et_test_tick_proc *tick_f;
+ et_test_finish_proc *finish_f;
+ et_test_translate_proc *trans_f;
+
+ const void *priv_data[4];
+};
+
+/* Array of tests. */
+extern const struct et_test et_tests[];
+extern const size_t et_ntests;
+
+#endif /* include guard */
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);