Reach hardware through backend records, and take the time from the host
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 2m46s
akbasic CI Build / sanitizers (push) Successful in 3m2s
akbasic CI Build / coverage (push) Failing after 3m8s
akbasic CI Build / mutation_test (push) Successful in 7m21s

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:
2026-07-31 08:07:23 -04:00
parent 5077efc3a7
commit 9b9dd09c5a
12 changed files with 888 additions and 1 deletions

171
tests/devices.c Normal file
View File

@@ -0,0 +1,171 @@
/**
* @file devices.c
* @brief Tests the device backend records, the palette table and the host clock.
*
* These are the pieces groups G, I and E are built on. Nothing here runs a BASIC
* verb -- that is what the per-group suites do -- it establishes that a runtime
* can be given backends, can be given none, and reports the time it was handed.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/graphics.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "mockdevice.h"
#include "testutil.h"
/**
* @brief A runtime with no backends attached must still come up and still print.
*
* This is the standalone driver's situation and the default for an embedding
* host that has not opted in, so it is the case that has to keep working.
*/
static void test_no_devices_is_normal(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE(HARNESS_RUNTIME.graphics == NULL, "graphics backend should start NULL");
TEST_REQUIRE(HARNESS_RUNTIME.audio == NULL, "audio backend should start NULL");
TEST_REQUIRE(HARNESS_RUNTIME.input == NULL, "input backend should start NULL");
TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 0);
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"HI\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HI\n");
harness_stop();
}
/** @brief Attaching backends stores them; attaching NULLs takes them away again. */
static void test_set_devices(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT));
TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend was not stored");
TEST_REQUIRE(HARNESS_RUNTIME.audio == &MOCK_AUDIO, "audio backend was not stored");
TEST_REQUIRE(HARNESS_RUNTIME.input == &MOCK_INPUT, "input backend was not stored");
/* Withholding a capability is spelled NULL, and must actually detach. */
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL));
TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend should have stayed");
TEST_REQUIRE(HARNESS_RUNTIME.audio == NULL, "audio backend should have been detached");
TEST_REQUIRE(HARNESS_RUNTIME.input == NULL, "input backend should have been detached");
TEST_REQUIRE_STATUS(akbasic_runtime_set_devices(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
harness_stop();
}
/** @brief The clock is stored verbatim, including a value that moves backwards. */
static void test_settime(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 1000);
/*
* A host driving a paused or scrubbed clock is allowed. Rejecting it would
* make the interpreter an authority on the host's own time, which it is not.
*/
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 400));
TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 400);
TEST_REQUIRE_STATUS(akbasic_runtime_settime(NULL, 0), AKERR_NULLPOINTER);
harness_stop();
}
/** @brief The palette covers 1 to 16 and refuses everything else. */
static void test_palette(void)
{
akbasic_Color color;
int index = 0;
memset(&color, 0, sizeof(color));
for ( index = 1; index <= 16; index++ ) {
TEST_REQUIRE_OK(akbasic_graphics_palette(index, &color));
TEST_REQUIRE_INT(color.a, 0xff);
}
/* Two entries pinned by value, so a shifted table is a failure and not a shrug. */
TEST_REQUIRE_OK(akbasic_graphics_palette(1, &color));
TEST_REQUIRE(color.r == 0x00 && color.g == 0x00 && color.b == 0x00,
"color 1 should be black, got %02x%02x%02x", color.r, color.g, color.b);
TEST_REQUIRE_OK(akbasic_graphics_palette(2, &color));
TEST_REQUIRE(color.r == 0xff && color.g == 0xff && color.b == 0xff,
"color 2 should be white, got %02x%02x%02x", color.r, color.g, color.b);
/*
* BASIC counts from 1. Zero is the off-by-one somebody writes when they
* forget that, and it has to be refused rather than quietly return slot 0.
*/
TEST_REQUIRE_STATUS(akbasic_graphics_palette(0, &color), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_graphics_palette(17, &color), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_graphics_palette(-1, &color), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_graphics_palette(1, NULL), AKERR_NULLPOINTER);
}
/** @brief The mock backends themselves record what they were told. */
static void test_mock_records(void)
{
akbasic_Color red = { 0xff, 0x00, 0x00, 0xff };
bool active = false;
int handle = -1;
int keycode = 0;
bool available = false;
mock_devices_init();
TEST_REQUIRE_OK(MOCK_GRAPHICS.point(&MOCK_GRAPHICS, 10.0, 20.0, red));
TEST_REQUIRE_OK(MOCK_GRAPHICS.line(&MOCK_GRAPHICS, 0.0, 0.0, 5.0, 5.0, red));
TEST_REQUIRE_STR(MOCK.log, "point 10.0,20.0 #ff0000\nline 0.0,0.0-5.0,5.0 #ff0000\n");
/* A saved shape yields a handle, and only a handle that exists can be pasted. */
TEST_REQUIRE_OK(MOCK_GRAPHICS.save_shape(&MOCK_GRAPHICS, 0, 0, 8, 8, &handle));
TEST_REQUIRE_INT(handle, 0);
TEST_REQUIRE_OK(MOCK_GRAPHICS.paste_shape(&MOCK_GRAPHICS, handle, 16.0, 16.0));
TEST_REQUIRE_STATUS(MOCK_GRAPHICS.paste_shape(&MOCK_GRAPHICS, 99, 0.0, 0.0), AKBASIC_ERR_BOUNDS);
/* The exhaustion switch is what lets the PAINT test reach the partial-fill path. */
MOCK.paint_exhausts = true;
TEST_REQUIRE_STATUS(MOCK_GRAPHICS.paint(&MOCK_GRAPHICS, 1, 1, red), AKERR_OUTOFBOUNDS);
MOCK.paint_exhausts = false;
TEST_REQUIRE_OK(MOCK_GRAPHICS.paint(&MOCK_GRAPHICS, 1, 1, red));
TEST_REQUIRE_OK(MOCK_AUDIO.tone(&MOCK_AUDIO, 0, 440.0, 250));
TEST_REQUIRE_OK(MOCK_AUDIO.voice_active(&MOCK_AUDIO, 0, &active));
TEST_REQUIRE(active, "voice 0 should be sounding after a tone");
TEST_REQUIRE_OK(MOCK_AUDIO.stop(&MOCK_AUDIO, 0));
TEST_REQUIRE_OK(MOCK_AUDIO.voice_active(&MOCK_AUDIO, 0, &active));
TEST_REQUIRE(!active, "voice 0 should be silent after a stop");
TEST_REQUIRE_STATUS(MOCK_AUDIO.tone(&MOCK_AUDIO, AKBASIC_AUDIO_VOICES, 440.0, 1),
AKBASIC_ERR_BOUNDS);
/* An empty key buffer is success with available false, never an error. */
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
TEST_REQUIRE(!available, "an empty mock buffer should report nothing available");
mock_push_keys("AB");
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
TEST_REQUIRE(available, "a primed mock buffer should report a key");
TEST_REQUIRE_INT(keycode, 'A');
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
TEST_REQUIRE_INT(keycode, 'B');
TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
TEST_REQUIRE(!available, "the mock buffer should drain to empty");
}
int main(void)
{
test_no_devices_is_normal();
test_set_devices();
test_settime();
test_palette();
test_mock_records();
return akbasic_test_failures;
}

View File

@@ -22,7 +22,7 @@ int main(void)
* "Unknown Error" in every stack trace that carries it, which nothing else
* in the suite would notice.
*/
for ( code = AKBASIC_ERR_SYNTAX; code <= AKBASIC_ERR_STATE; code++ ) {
for ( code = AKBASIC_ERR_SYNTAX; code < AKBASIC_ERR_LAST; code++ ) {
const char *name = akerr_name_for_status(code, NULL);
TEST_REQUIRE(name != NULL, "status %d has a NULL name", code);
TEST_REQUIRE(strcmp(name, "Unknown Error") != 0,
@@ -31,6 +31,16 @@ int main(void)
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_SYNTAX, NULL), "Syntax Error");
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_STATE, NULL), "State Error");
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_DEVICE, NULL), "Device Error");
/*
* The sentinel must stay inside the band it terminates. If somebody ever adds
* 250 codes, the loop above walks straight out of our reservation and starts
* asserting on names libakgl or nobody owns.
*/
TEST_REQUIRE(AKBASIC_ERR_LAST < AKBASIC_ERR_LIMIT,
"AKBASIC_ERR_LAST %d has run past the reserved band ending at %d",
AKBASIC_ERR_LAST, AKBASIC_ERR_LIMIT);
/*
* The assertion that proves the range is actually ours rather than merely

333
tests/mockdevice.h Normal file
View 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_