Check the renderer backend before drawing text
akgl_text_rendertextat() called renderer->draw_texture() with no check on the global renderer, its sdl_renderer or the function pointer itself, so a backend that has an SDL_Renderer but was never bound segfaults on the first line of text -- the state akgl_render_bind2d() exists to get a host out of. All three are now checked, with AKERR_NULLPOINTER as the draw entry points report, and checked before anything is rasterized rather than after. tests/text.c grows a software renderer with a bound backend and covers all three refusals plus the successful draw, wrapped and unwrapped: src/text.c goes from 58% to 100% line coverage. A made-up SDL_Renderer pointer is not enough to test this -- SDL refuses the bogus handle on its own and the call fails for the wrong reason, which a deleted check survives. Also documents what the tests found: SDL_ttf refuses the empty string, so drawing an empty line is an error while measuring one is not. Filed in TODO.md rather than pinned in the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
111
tests/text.c
111
tests/text.c
@@ -2,9 +2,11 @@
|
||||
* @file text.c
|
||||
* @brief Unit tests for font loading and text measurement.
|
||||
*
|
||||
* Measurement needs a font but no renderer, so this suite runs without a window
|
||||
* and without the offscreen harness the drawing half of src/text.c is waiting
|
||||
* on. akgl_text_rendertextat() is therefore not covered here.
|
||||
* Measurement needs a font but no renderer at all. Drawing needs one, but not a
|
||||
* display: a software renderer over a window under the dummy video driver takes
|
||||
* akgl_text_rendertextat() all the way from rasterizing to blitting, so this
|
||||
* suite covers it without waiting for the offscreen harness. What is still
|
||||
* missing is any assertion about the *pixels* -- that is the harness's job.
|
||||
*
|
||||
* The fixture font is monospaced on purpose: the width of an N-character string
|
||||
* is exactly N times the width of one character, so every assertion below is a
|
||||
@@ -18,7 +20,9 @@
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/text.h>
|
||||
|
||||
#include "testutil.h"
|
||||
@@ -27,9 +31,13 @@
|
||||
#define TEST_FONT_PATH "assets/akgl_test_mono.ttf"
|
||||
/** @brief Point size every test in this file opens the fixture font at. */
|
||||
#define TEST_FONT_SIZE 16
|
||||
/** @brief Width and height of the offscreen target the drawing tests draw into. */
|
||||
#define TEST_TARGET_SIZE 128
|
||||
|
||||
/** @brief The fixture font, opened once by main() and shared by every test. */
|
||||
static TTF_Font *testfont = NULL;
|
||||
/** @brief A 2D backend bound to a software renderer, built by main(). */
|
||||
static akgl_RenderBackend testbackend;
|
||||
|
||||
akerr_ErrorContext *test_text_loadfont(void)
|
||||
{
|
||||
@@ -214,18 +222,94 @@ akerr_ErrorContext *test_text_measure_wrapped(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_text_render_backend_guards(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_RenderBackend backend;
|
||||
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
|
||||
|
||||
ATTEMPT {
|
||||
// Nothing initialized the renderer at all, which is where the global
|
||||
// starts and where a host that has not called akgl_game_init leaves it.
|
||||
renderer = NULL;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
|
||||
"drawing text with no renderer backend");
|
||||
|
||||
// A backend that exists but was never given an SDL_Renderer -- the
|
||||
// state a host is in between allocating one and initializing it.
|
||||
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
|
||||
renderer = &backend;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
|
||||
"drawing text through a backend with no SDL renderer");
|
||||
|
||||
// A live SDL_Renderer but no methods: a host that created its own
|
||||
// renderer and has not called akgl_render_bind2d() yet. This is the one
|
||||
// that used to be a segfault rather than an error -- with a real
|
||||
// renderer behind it the call gets all the way to a NULL function
|
||||
// pointer, which is why the check has to be here and not left to SDL.
|
||||
backend.sdl_renderer = testbackend.sdl_renderer;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
|
||||
"drawing text through a backend that was never bound");
|
||||
|
||||
renderer = &testbackend;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(NULL, "hello", white, 0, 0, 0),
|
||||
"drawing text with a NULL font");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, NULL, white, 0, 0, 0),
|
||||
"drawing a NULL string");
|
||||
} CLEANUP {
|
||||
renderer = &testbackend;
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_text_rendertextat(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
|
||||
|
||||
ATTEMPT {
|
||||
// A bound backend over a software renderer is enough to draw through:
|
||||
// no display, no window shown, and the whole path from rasterizing to
|
||||
// blitting runs.
|
||||
renderer = &testbackend;
|
||||
TEST_EXPECT_OK(errctx, renderer->frame_start(renderer), "starting a frame");
|
||||
TEST_EXPECT_OK(errctx, akgl_text_rendertextat(testfont, "hello", white, 0, 4, 4),
|
||||
"drawing a line of text");
|
||||
// Wrapping takes the other rasterizing path.
|
||||
TEST_EXPECT_OK(errctx,
|
||||
akgl_text_rendertextat(testfont, "one two three", white, TEST_TARGET_SIZE / 2, 0, 0),
|
||||
"drawing wrapped text");
|
||||
// Not asserted here: the empty string, which SDL_ttf refuses with "Text
|
||||
// has zero width" on both paths, so drawing an empty line reports a
|
||||
// failure rather than drawing nothing. That is filed rather than pinned
|
||||
// -- see TODO.md, "Known and still open".
|
||||
TEST_EXPECT_OK(errctx, renderer->frame_end(renderer), "ending the frame");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(0),
|
||||
SDL_Init(SDL_INIT_VIDEO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
@@ -245,9 +329,28 @@ int main(void)
|
||||
TEST_FONT_PATH,
|
||||
SDL_GetError());
|
||||
|
||||
// The host owns the window and libakgl binds to it, which is the
|
||||
// arrangement akgl_render_bind2d exists for and the one an embedded
|
||||
// interpreter drawing text is in.
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(
|
||||
"net/aklabs/libakgl/test_text",
|
||||
TEST_TARGET_SIZE,
|
||||
TEST_TARGET_SIZE,
|
||||
0,
|
||||
&window,
|
||||
&testbackend.sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
CATCH(errctx, akgl_render_bind2d(&testbackend));
|
||||
|
||||
CATCH(errctx, test_text_loadfont());
|
||||
CATCH(errctx, test_text_measure());
|
||||
CATCH(errctx, test_text_measure_wrapped());
|
||||
CATCH(errctx, test_text_render_backend_guards());
|
||||
CATCH(errctx, test_text_rendertextat());
|
||||
} CLEANUP {
|
||||
if ( testfont != NULL ) {
|
||||
TTF_CloseFont(testfont);
|
||||
|
||||
Reference in New Issue
Block a user