/** * @file text.c * @brief Unit tests for font loading and text measurement. * * 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 * relationship between measurements rather than a hardcoded pixel count that * would break when FreeType changes its rounding. */ #include #include #include #include #include #include #include #include #include #include "testutil.h" /** @brief The monospaced ASCII subset described in assets/akgl_test_mono.LICENSE.txt. */ #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) { PREPARE_ERROR(errctx); TTF_Font *registered = NULL; ATTEMPT { CATCH(errctx, akgl_registry_init_font()); TEST_EXPECT_OK( errctx, akgl_text_loadfont("testfont", TEST_FONT_PATH, TEST_FONT_SIZE), "loading the fixture font"); registered = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL); TEST_ASSERT(errctx, registered != NULL, "akgl_text_loadfont did not place the font in AKGL_REGISTRY_FONT"); TEST_ASSERT(errctx, TTF_GetFontHeight(registered) > 0, "the registered font reports height %d, expected a positive height", TTF_GetFontHeight(registered)); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_loadfont(NULL, TEST_FONT_PATH, TEST_FONT_SIZE), "loading a font under a NULL name"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_loadfont("nullpath", NULL, TEST_FONT_SIZE), "loading a font from a NULL path"); TEST_EXPECT_STATUS(errctx, AKGL_ERR_SDL, akgl_text_loadfont("missing", "assets/no_such_font.ttf", TEST_FONT_SIZE), "loading a font that does not exist"); // Loading over a live name closes the font it displaces. Nothing here // can observe the close directly -- the proof is the memcheck run -- // but the replacement itself is observable, and it is the path that // used to abandon ten kilobytes per call. TEST_EXPECT_OK( errctx, akgl_text_loadfont("testfont", TEST_FONT_PATH, TEST_FONT_SIZE), "loading a second font over a registered name"); TEST_ASSERT(errctx, SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL) != registered, "reloading a font under a live name left the displaced font registered"); // And the font goes back. Unloading is what makes the suite's own // bookkeeping honest, so this is not only an API test. TEST_EXPECT_OK(errctx, akgl_text_unloadfont("testfont"), "unloading a registered font"); TEST_ASSERT(errctx, SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL) == NULL, "akgl_text_unloadfont left the font in AKGL_REGISTRY_FONT"); TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_text_unloadfont("testfont"), "unloading a font that is already gone"); TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_text_unloadfont("never_loaded"), "unloading a name that was never registered"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_unloadfont(NULL), "unloading a NULL name"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_text_measure(void) { PREPARE_ERROR(errctx); int w = 0; int h = 0; int onecharw = 0; int onecharh = 0; int emptyw = 0; int emptyh = 0; ATTEMPT { // One cell. This is the measurement a character grid is built from. TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "A", &onecharw, &onecharh), "measuring a single character"); TEST_ASSERT(errctx, onecharw > 0, "one character measured %d wide, expected a positive width", onecharw); TEST_ASSERT(errctx, onecharh == TTF_GetFontHeight(testfont), "one character measured %d high, expected the font height %d", onecharh, TTF_GetFontHeight(testfont)); // The font is monospaced, so four cells are exactly four times one. TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "AAAA", &w, &h), "measuring four characters"); TEST_ASSERT(errctx, w == (onecharw * 4), "four characters measured %d wide, expected %d", w, onecharw * 4); TEST_ASSERT(errctx, h == onecharh, "four characters on one line measured %d high, expected %d", h, onecharh); // ...and every character advances by the same amount, which is what // makes a fixed grid legitimate in the first place. TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "W", &w, &h), "measuring a wide glyph"); TEST_ASSERT(errctx, w == onecharw, "'W' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw); TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "i", &w, &h), "measuring a narrow glyph"); TEST_ASSERT(errctx, w == onecharw, "'i' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw); // The empty string is zero wide and still one line high, so a cursor // sitting on an empty line has somewhere to be. TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "", &emptyw, &emptyh), "measuring the empty string"); TEST_ASSERT(errctx, emptyw == 0, "the empty string measured %d wide, expected 0", emptyw); TEST_ASSERT(errctx, emptyh == onecharh, "the empty string measured %d high, expected the font height %d", emptyh, onecharh); // Measuring does not disturb the destinations it was not asked about. w = -1; h = -1; TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "hello", &w, &h), "measuring a word"); TEST_ASSERT(errctx, w == (onecharw * 5), "\"hello\" measured %d wide, expected %d", w, onecharw * 5); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(NULL, "A", &w, &h), "measuring with a NULL font"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, NULL, &w, &h), "measuring a NULL string"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", NULL, &h), "measuring into a NULL width"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", &w, NULL), "measuring into a NULL height"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_text_measure_wrapped(void) { PREPARE_ERROR(errctx); int w = 0; int h = 0; int onecharw = 0; int flatw = 0; int flath = 0; int lineskip = 0; ATTEMPT { CATCH(errctx, akgl_text_measure(testfont, "A", &onecharw, &flath)); lineskip = TTF_GetFontLineSkip(testfont); TEST_ASSERT(errctx, lineskip > 0, "the font reports a line skip of %d", lineskip); // A wrap length wide enough for the whole string measures the same as // the unwrapped call. CATCH(errctx, akgl_text_measure(testfont, "one two", &flatw, &flath)); TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one two", flatw + onecharw, &w, &h), "measuring a string that fits inside the wrap length"); TEST_ASSERT(errctx, w == flatw, "an unwrapped measurement gave %d wide, expected %d", w, flatw); TEST_ASSERT(errctx, h == flath, "an unwrapped measurement gave %d high, expected %d", h, flath); // Narrow enough to force a break at the space: two lines, and nothing // wider than the wrap length. TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one two", onecharw * 4, &w, &h), "measuring a string that has to wrap"); TEST_ASSERT(errctx, w <= (onecharw * 4), "a wrapped measurement gave %d wide, past the %d wrap length", w, onecharw * 4); TEST_ASSERT(errctx, h >= (lineskip * 2), "a wrapped measurement gave %d high, expected at least two lines (%d)", h, lineskip * 2); // Zero wraps on newlines only, so an embedded newline still costs a line // and a long unbroken string does not. TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one\ntwo", 0, &w, &h), "measuring a string with an embedded newline"); TEST_ASSERT(errctx, h >= (lineskip * 2), "an embedded newline measured %d high, expected at least two lines (%d)", h, lineskip * 2); TEST_ASSERT(errctx, w == (onecharw * 3), "the longer of two three-character lines measured %d wide, expected %d", w, onecharw * 3); TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one two", 0, &w, &h), "measuring a string with no newline at wrap length zero"); TEST_ASSERT(errctx, h == flath, "a string with no newline measured %d high at wrap length 0, expected %d", h, flath); // A negative wrap length is refused rather than silently treated as // "never wrap", which is what SDL_ttf does with it. TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_text_measure_wrapped(testfont, "one two", -1, &w, &h), "measuring at a negative wrap length"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure_wrapped(NULL, "A", 100, &w, &h), "measuring wrapped with a NULL font"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure_wrapped(testfont, NULL, 100, &w, &h), "measuring a NULL string wrapped"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure_wrapped(testfont, "A", 100, NULL, &h), "measuring wrapped into a NULL width"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure_wrapped(testfont, "A", 100, &w, NULL), "measuring wrapped into a NULL height"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); 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(SDL_INIT_VIDEO), AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_ZERO_BREAK( errctx, TTF_Init(), AKGL_ERR_SDL, "Couldn't initialize the font engine: %s", SDL_GetError()); testfont = TTF_OpenFont(TEST_FONT_PATH, TEST_FONT_SIZE); FAIL_ZERO_BREAK( errctx, testfont, AKGL_ERR_SDL, "Couldn't open %s: %s", 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); } TTF_Quit(); SDL_Quit(); } PROCESS(errctx) { } FINISH_NORETURN(errctx); }