From 40724faccacf055cd9f9bba4615b9f930be9b76b Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Sat, 27 Jun 2026 22:53:26 -0500 Subject: initial commit --- .clangd | 13 ++ .gitignore | 4 + Makefile | 138 +++++++++++++++++++ fire-pit/test.c | 49 +++++++ fire-pit/test.h | 77 +++++++++++ source/macros.h | 12 ++ source/state.h | 57 ++++++++ source/states/menu.c | 96 ++++++++++++++ source/template.c | 303 ++++++++++++++++++++++++++++++++++++++++++ source/term.h | 13 ++ source/test-impl.inc.h | 49 +++++++ source/test.c | 354 +++++++++++++++++++++++++++++++++++++++++++++++++ source/test.h | 120 +++++++++++++++++ source/tests/basic.c | 66 +++++++++ 14 files changed, 1351 insertions(+) create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 fire-pit/test.c create mode 100644 fire-pit/test.h create mode 100644 source/macros.h create mode 100644 source/state.h create mode 100644 source/states/menu.c create mode 100644 source/template.c create mode 100644 source/term.h create mode 100644 source/test-impl.inc.h create mode 100644 source/test.c create mode 100644 source/test.h create mode 100644 source/tests/basic.c 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=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 + +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 +#include + +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 +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +#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 +#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 +#include +#include +#include +#include +#include + +#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 +#include +#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 + +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); -- cgit v1.2.3-70-g09d2