/** * @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. * * 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 "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 The fixture font, opened once by main() and shared by every test. */ static TTF_Font *testfont = NULL; 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"); } 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); } int main(void) { PREPARE_ERROR(errctx); SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { CATCH(errctx, akgl_error_init()); FAIL_ZERO_BREAK( errctx, SDL_Init(0), 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()); CATCH(errctx, test_text_loadfont()); CATCH(errctx, test_text_measure()); CATCH(errctx, test_text_measure_wrapped()); } CLEANUP { if ( testfont != NULL ) { TTF_CloseFont(testfont); } TTF_Quit(); SDL_Quit(); } PROCESS(errctx) { } FINISH_NORETURN(errctx); }