Reach hardware through backend records, and take the time from the host
Groups G, I and E are unblocked but cannot be written yet: the core library is free of SDL and builds with no libakgl present, so a graphics verb cannot call akgl_draw_* and a sound verb cannot call akgl_audio_*. This adds what they call instead. Three records of function pointers -- akbasic_GraphicsBackend, _AudioBackend and _InputBackend -- in the same shape as akbasic_TextSink, and the same shape libakgl uses for akgl_RenderBackend. akbasic_runtime_set_devices() attaches any subset; all three may be NULL and that is the standalone driver's normal state, so a runtime with no backends still comes up and still prints. A verb that needs one it was not given raises the new AKBASIC_ERR_DEVICE rather than dereferencing a NULL vtable. Two decisions worth stating. The graphics record has no circle entry point: BASIC 7.0's CIRCLE takes two radii, an arc range, a rotation and a degree increment, which makes it a polygon by definition, so it will be built from line calls rather than from akgl_draw_circle. And coordinates are double rather than an integer pixel address, because SCALE makes them fractional and rounding at each verb rather than once at the backend accumulates drift along a polyline. akbasic_runtime_settime() is how SOUND, PLAY and TEMPO get a clock without the library reading one. Section 1.6 forbids blocking or owning a loop, so the caller that owns the frame owns the time -- which is what libakgl already does, since akgl_actor_logic_changeframe takes curtimems as an argument. Left unset it is zero and every duration expires immediately: audible, but never a hang. AKBASIC_ERR_LAST is a sentinel rather than a status, so tests/error_codes.c walks every code looking for an unnamed one without anybody remembering to widen the loop when a code is added. tests/mockdevice.h records every backend call as a formatted line. The graphics and audio verbs emit nothing a golden file can compare, so that log is where their assertions have to live -- and since it needs no SDL, the whole of groups G, I and E stays testable in the default build. 62/62 ctest, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
333
tests/mockdevice.h
Normal file
333
tests/mockdevice.h
Normal file
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* @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 <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/audio.h>
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/graphics.h>
|
||||
#include <akbasic/input.h>
|
||||
|
||||
/** @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_
|
||||
Reference in New Issue
Block a user