/** * @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 #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 */ int devwidth; /* what size() reports; see mock_devices_init */ int devheight; /* Audio */ bool voice_active[AKBASIC_AUDIO_VOICES]; /* Input */ int keys[MOCK_MAX_KEYS]; int keycount; int keynext; /* Sprites */ uint16_t collisions; /* what the next collisions() call reports */ int patternbytes[AKBASIC_MAX_SPRITES]; /* bytes the last define() carried, per sprite */ } akbasic_MockDevice; static akbasic_MockDevice MOCK; static akbasic_GraphicsBackend MOCK_GRAPHICS; static akbasic_AudioBackend MOCK_AUDIO; static akbasic_InputBackend MOCK_INPUT; static akbasic_SpriteBackend MOCK_SPRITES; /** * @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 -- */ /** * @brief Report the recorded surface size, and log that it was asked. * * Logged like every other call because *when* the interpreter asks matters: it * re-asks before each verb that draws, deliberately, so a resized window is not * drawn to at its old size. */ static akerr_ErrorContext *mock_size(akbasic_GraphicsBackend *self, int *width, int *height) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (width != NULL && height != NULL), AKERR_NULLPOINTER, "NULL destination in mock_size"); *width = MOCK.devwidth; *height = MOCK.devheight; SUCCEED_RETURN(errctx); } 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_sweep(akbasic_AudioBackend *self, int voice, double from_hz, double to_hz, double step_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("sweep v%d %.1fhz->%.1fhz step %.1fhz %dms\n", voice, from_hz, to_hz, step_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); } /* ---------------------------------------------------------------- sprites -- */ /** * @brief Refuse a sprite number the way a real backend has to. * * Sprites are 1-based at this boundary -- see sprite.h -- so an off-by-one in a * verb handler shows up here as AKBASIC_ERR_BOUNDS rather than as a silently * mis-addressed sprite. */ static akerr_ErrorContext AKERR_NOIGNORE *mock_sprite_range(int n) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS, "mock sprite %d out of range", n); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_define(akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg) { PREPARE_ERROR(errctx); (void)self; PASS(errctx, mock_sprite_range(n)); FAIL_ZERO_RETURN(errctx, (pattern != NULL), AKERR_NULLPOINTER, "NULL pattern in define"); MOCK.patternbytes[n - 1] = bytes; /* * The first and last bytes rather than all 63: enough to tell one pattern * from another and to catch a byte order that came through reversed, * without a log line no human can read. */ mock_log("sprdef %d %d bytes %02x..%02x fg=%d,%d,%d bg=%d,%d,%d\n", n, bytes, pattern[0], pattern[bytes - 1], fg.r, fg.g, fg.b, bg.r, bg.g, bg.b); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_define_shape(akbasic_SpriteBackend *self, int n, int handle) { PREPARE_ERROR(errctx); (void)self; PASS(errctx, mock_sprite_range(n)); mock_log("sprshape %d from %d\n", n, handle); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_define_file(akbasic_SpriteBackend *self, int n, const char *path, const char *root) { PREPARE_ERROR(errctx); (void)self; PASS(errctx, mock_sprite_range(n)); FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER, "NULL path in define_file"); /* * The root is logged as well as the path. Whether the interpreter passed the * running program's directory through is the whole of what this repository * owns about relative asset paths -- resolving one is libakgl's job, and * there is no way to see it happen from a mock. */ mock_log("sprfile %d %s root=%s\n", n, path, (root != NULL ? root : "(none)")); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_show(akbasic_SpriteBackend *self, int n, bool visible) { PREPARE_ERROR(errctx); (void)self; PASS(errctx, mock_sprite_range(n)); mock_log("sprshow %d %d\n", n, (visible ? 1 : 0)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_move(akbasic_SpriteBackend *self, int n, double x, double y) { PREPARE_ERROR(errctx); (void)self; PASS(errctx, mock_sprite_range(n)); mock_log("sprmove %d %.2f,%.2f\n", n, x, y); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_configure(akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind) { PREPARE_ERROR(errctx); (void)self; PASS(errctx, mock_sprite_range(n)); mock_log("sprcfg %d %d,%d,%d x%d y%d b%d\n", n, color.r, color.g, color.b, (xexpand ? 1 : 0), (yexpand ? 1 : 0), (behind ? 1 : 0)); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_shared_colors(akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2) { PREPARE_ERROR(errctx); (void)self; mock_log("sprshared %d,%d,%d %d,%d,%d\n", c1.r, c1.g, c1.b, c2.r, c2.g, c2.b); SUCCEED_RETURN(errctx); } static akerr_ErrorContext *mock_spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask) { PREPARE_ERROR(errctx); (void)self; FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions"); *mask = MOCK.collisions; 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)); /* * 640x400 rather than 320x200, on purpose: it is exactly twice the fallback, * so a scaled coordinate that came out right by accident -- because nothing * consulted the device at all -- is off by a factor of two rather than * indistinguishable. A test that wants the fallback clears MOCK_GRAPHICS.size, * the way the SOUND test clears MOCK_AUDIO.sweep. */ MOCK.devwidth = 640; MOCK.devheight = 400; MOCK_GRAPHICS.self = &MOCK; MOCK_GRAPHICS.size = mock_size; 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; /* * Set here, and deliberately cleared again by the one test that needs a * backend without it: `sweep` may be NULL, which is what a host written * against libakgl 0.2.0 looks like, and SOUND has to refuse rather than * crash on one. */ MOCK_AUDIO.sweep = mock_sweep; 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; MOCK_SPRITES.self = &MOCK; MOCK_SPRITES.define = mock_spr_define; MOCK_SPRITES.define_shape = mock_spr_define_shape; MOCK_SPRITES.define_file = mock_spr_define_file; MOCK_SPRITES.show = mock_spr_show; MOCK_SPRITES.move = mock_spr_move; MOCK_SPRITES.configure = mock_spr_configure; MOCK_SPRITES.shared_colors = mock_spr_shared_colors; MOCK_SPRITES.collisions = mock_spr_collisions; } /** @brief Set what the next collisions() call will report. Bit n-1 is sprite n. */ __attribute__((unused)) static void mock_set_collisions(uint16_t mask) { MOCK.collisions = mask; } /** @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_