Files
akbasic/tests/error_codes.c
Andrew Kesterson 9b9dd09c5a
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
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>
2026-07-31 08:07:23 -04:00

61 lines
2.2 KiB
C

/**
* @file error_codes.c
* @brief Tests that akbasic owns its status band and has named every code in it.
*/
#include <string.h>
#include <akbasic/error.h>
#include "testutil.h"
int main(void)
{
int code = 0;
/* Registering is idempotent: an identical repeat by the same owner is a no-op. */
TEST_REQUIRE_OK(akbasic_error_register());
TEST_REQUIRE_OK(akbasic_error_register());
/*
* Every code must read back a real name. An unnamed one degrades to
* "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_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,
"status %d reads back as \"Unknown Error\"", code);
}
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
* unclaimed: a different owner must be refused.
*/
TEST_REQUIRE_STATUS(akerr_register_status_name("not-akbasic", AKBASIC_ERR_SYNTAX, "Hijacked"),
AKERR_STATUS_NAME_FOREIGN);
TEST_REQUIRE_STATUS(akerr_reserve_status_range(AKBASIC_ERR_BASE, 4, "not-akbasic"),
AKERR_STATUS_RANGE_OVERLAP);
/* The band must sit clear of libakerror's reserved 0-255. */
TEST_REQUIRE(AKBASIC_ERR_BASE >= AKERR_FIRST_CONSUMER_STATUS,
"AKBASIC_ERR_BASE %d is inside libakerror's reserved band",
AKBASIC_ERR_BASE);
return akbasic_test_failures;
}