/** * @file mockdevice.h * @brief Recording graphics, audio and input backends for the device tests. * * The graphics and audio verbs produce no output a golden file can compare, so * the assertions have to be about what reached the backend. These record every * call as a formatted line, which a test compares against the sequence it * expected -- what was drawn, in what order, in what color. * * Nothing here needs SDL, which is the point of the backend records in the first * place: the whole of groups G, I and E is testable in the default build. */ #ifndef _AKBASIC_TEST_MOCKDEVICE_H_ #define _AKBASIC_TEST_MOCKDEVICE_H_ #include #include #include #include #include #include #include #include /** @brief Room for the call log. Longer than any test needs; overflow truncates loudly. */ #define MOCK_LOG_SIZE 8192 /** @brief How many keystrokes mock_input can be primed with. */ #define MOCK_MAX_KEYS 32 /** @brief How many regions mock_graphics will hand out handles for. */ #define MOCK_MAX_SHAPES 8 /** @brief State shared by all three mock backends, so one log records the lot. */ typedef struct { char log[MOCK_LOG_SIZE]; size_t used; bool overflowed; /* Graphics */ int shapes; /* handles handed out so far */ bool paint_exhausts; /* when true, paint reports a partial fill */ /* Audio */ bool voice_active[AKBASIC_AUDIO_VOICES]; /* Input */ int keys[MOCK_MAX_KEYS]; int keycount; int keynext; } akbasic_MockDevice; static akbasic_MockDevice MOCK; static akbasic_GraphicsBackend MOCK_GRAPHICS; static akbasic_AudioBackend MOCK_AUDIO; static akbasic_InputBackend MOCK_INPUT; /** * @brief Append one formatted call to the log. * * Deliberately not an akerr_ErrorContext * function. It is called from inside * the vtable entry points, and a test whose log overflowed should say so through * the assertion on the log contents rather than by raising an error that the * verb under test would then report as a device failure. */ __attribute__((format(printf, 1, 2))) static void mock_log(const char *format, ...) { va_list args; int written = 0; if ( MOCK.used >= sizeof(MOCK.log) - 1 ) { MOCK.overflowed = true; return; } va_start(args, format); written = vsnprintf(MOCK.log + MOCK.used, sizeof(MOCK.log) - MOCK.used, format, args); va_end(args); if ( written < 0 || (size_t)written >= sizeof(MOCK.log) - MOCK.used ) { MOCK.overflowed = true; MOCK.used = sizeof(MOCK.log) - 1; return; } MOCK.used += (size_t)written; } /* --------------------------------------------------------------- graphics -- */ static akerr_ErrorContext *mock_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color) { PREPARE_ERROR(errctx); (void)self; mock_log("point %.1f,%.1f #%02x%02x%02x\n", x, y, color.r, color.g, color.b); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color) { PREPARE_ERROR(errctx); (void)self; mock_log("line %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color) { PREPARE_ERROR(errctx); (void)self; mock_log("rect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color) { PREPARE_ERROR(errctx); (void)self; mock_log("fillrect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color) { PREPARE_ERROR(errctx); (void)self; mock_log("paint %d,%d #%02x%02x%02x\n", x, y, color.r, color.g, color.b); /* * Stands in for the akgl flood fill running out of its fixed span stack, * which reports AKERR_OUTOFBOUNDS having filled part of the region. PAINT has * to surface that rather than treat it as success. */ FAIL_ZERO_RETURN(errctx, (!MOCK.paint_exhausts), AKERR_OUTOFBOUNDS, "mock span stack exhausted"); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_clear(akbasic_GraphicsBackend *self, akbasic_Color color) { PREPARE_ERROR(errctx); (void)self; mock_log("clear #%02x%02x%02x\n", color.r, color.g, color.b); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER, "NULL handle in save_shape"); FAIL_ZERO_RETURN(errctx, (MOCK.shapes < MOCK_MAX_SHAPES), AKBASIC_ERR_DEVICE, "mock shape pool exhausted"); *handle = MOCK.shapes; MOCK.shapes += 1; mock_log("save %d,%d-%d,%d -> %d\n", x1, y1, x2, y2, *handle); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < MOCK.shapes), AKBASIC_ERR_BOUNDS, "no such mock shape %d", handle); mock_log("paste %d at %.1f,%.1f\n", handle, x, y); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_free_shapes(akbasic_GraphicsBackend *self) { PREPARE_ERROR(errctx); (void)self; MOCK.shapes = 0; mock_log("freeshapes\n"); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------------ audio -- */ static akerr_ErrorContext *mock_tone(akbasic_AudioBackend *self, int voice, double hz, int ms) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS, "mock voice %d out of range", voice); MOCK.voice_active[voice] = true; mock_log("tone v%d %.1fhz %dms\n", voice, hz, ms); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_audio_stop(akbasic_AudioBackend *self, int voice) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS, "mock voice %d out of range", voice); MOCK.voice_active[voice] = false; mock_log("stop v%d\n", voice); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS, "mock voice %d out of range", voice); mock_log("wave v%d %d\n", voice, (int)waveform); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS, "mock voice %d out of range", voice); mock_log("env v%d %d/%d/%.2f/%d\n", voice, attack, decay, sustain, release); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_volume(akbasic_AudioBackend *self, double level) { PREPARE_ERROR(errctx); (void)self; mock_log("vol %.2f\n", level); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_voice_active(akbasic_AudioBackend *self, int voice, bool *active) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (active != NULL), AKERR_NULLPOINTER, "NULL active in voice_active"); FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS, "mock voice %d out of range", voice); *active = MOCK.voice_active[voice]; SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------------ input -- */ static akerr_ErrorContext *mock_poll_key(akbasic_InputBackend *self, int *keycode, bool *available) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (keycode != NULL && available != NULL), AKERR_NULLPOINTER, "NULL argument in poll_key"); if ( MOCK.keynext >= MOCK.keycount ) { /* An empty buffer is success, not an error -- see input.h. */ *available = false; *keycode = 0; SUCCEED_RETURN(errctx); } *keycode = MOCK.keys[MOCK.keynext]; *available = true; MOCK.keynext += 1; SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_flush_keys(akbasic_InputBackend *self) { PREPARE_ERROR(errctx); (void)self; MOCK.keynext = MOCK.keycount; mock_log("flushkeys\n"); SUCCEED_RETURN(errctx); } /* ---------------------------------------------------------------- fixture -- */ /** @brief Reset the recorder and populate all three vtables. */ __attribute__((unused)) static void mock_devices_init(void) { memset(&MOCK, 0, sizeof(MOCK)); MOCK_GRAPHICS.self = &MOCK; MOCK_GRAPHICS.point = mock_point; MOCK_GRAPHICS.line = mock_line; MOCK_GRAPHICS.rect = mock_rect; MOCK_GRAPHICS.filled_rect = mock_filled_rect; MOCK_GRAPHICS.paint = mock_paint; MOCK_GRAPHICS.clear = mock_clear; MOCK_GRAPHICS.save_shape = mock_save_shape; MOCK_GRAPHICS.paste_shape = mock_paste_shape; MOCK_GRAPHICS.free_shapes = mock_free_shapes; MOCK_AUDIO.self = &MOCK; MOCK_AUDIO.tone = mock_tone; MOCK_AUDIO.stop = mock_audio_stop; MOCK_AUDIO.waveform = mock_waveform; MOCK_AUDIO.envelope = mock_envelope; MOCK_AUDIO.volume = mock_volume; MOCK_AUDIO.voice_active = mock_voice_active; MOCK_INPUT.self = &MOCK; MOCK_INPUT.poll_key = mock_poll_key; MOCK_INPUT.flush_keys = mock_flush_keys; } /** @brief Prime the input backend with keystrokes GET will drain in order. */ __attribute__((unused)) static void mock_push_keys(const char *keys) { size_t i = 0; MOCK.keycount = 0; MOCK.keynext = 0; for ( i = 0; keys != NULL && keys[i] != '\0' && MOCK.keycount < MOCK_MAX_KEYS; i++ ) { MOCK.keys[MOCK.keycount] = (int)(unsigned char)keys[i]; MOCK.keycount += 1; } } /** @brief Forget everything logged so far, keeping the primed state. */ __attribute__((unused)) static void mock_log_reset(void) { memset(MOCK.log, 0, sizeof(MOCK.log)); MOCK.used = 0; MOCK.overflowed = false; } #endif // _AKBASIC_TEST_MOCKDEVICE_H_