diff --git a/indra/llrender/CMakeLists.txt b/indra/llrender/CMakeLists.txt index e7eb2965ef..ab6c6cb3af 100644 --- a/indra/llrender/CMakeLists.txt +++ b/indra/llrender/CMakeLists.txt @@ -133,64 +133,96 @@ if(AL_BUILD_TESTS) "LLFONT_TEST_FONTS_XML=\"${font_test_fonts_xml}\"" ) - # Arithmetic only: where a local coordinate lands under the UI transform. - al_add_test(lluitransform PROJECT llrender LIBRARIES llrender) + # The state guard's checks on what llrender keeps for the process, for + # each of its test binaries. + add_library(llrender_test_checks OBJECT tests/llrender_test_checks.cpp) + target_link_libraries(llrender_test_checks PRIVATE al::flags al_test_support llrender) + set_target_properties(llrender_test_checks PROPERTIES FOLDER "Tests/llrender") - # Arithmetic only: the matrix stack, the projection helpers and the - # clip-plane classes against the scalar matrix and the formulas. - al_add_test(llrendermatrix PROJECT llrender LIBRARIES llrender) - - # The font suites: tables and shaping, no GL. - foreach(test alfontcolrv1 llfontregistry llfontbitmapcache llfontfreetype) - al_add_test(${test} PROJECT llrender LIBRARIES llrender DEFINES ${font_defines}) - endforeach() - # Both Unicode tables in one place, so the test can hold them against each - # other. llrender itself has no need of ICU. - al_add_test(alfontshaping PROJECT llrender LIBRARIES llrender ll::icu DEFINES ${font_defines}) - - # The GL-backed suites, on LLWindowManager's hidden window: the paths of - # the four font libraries above that rasterize, and the wholly-GL ones. - foreach( - test - alfontcolrv1_gl - llfontregistry_gl - llfontbitmapcache_gl - llfontfreetype_gl - llfontgl - llfonttextcache - llimagegl + # The suites that need no GL: arithmetic (where a local coordinate lands + # under the UI transform; the matrix stack, the projection helpers and the + # clip-plane classes), and the font tables and shaping. alfontshaping holds + # both Unicode tables against each other, so this binary links ICU, which + # llrender itself has no need of. + al_add_test_binary( + llrender_tests + PROJECT llrender + SOURCES + tests/alfontcolrv1_test.cpp + tests/alfontshaping_test.cpp + tests/llfontbitmapcache_test.cpp + tests/llfontfreetype_test.cpp + tests/llfontregistry_test.cpp + tests/llrendermatrix_test.cpp + tests/lluitransform_test.cpp + LIBRARIES llrender ll::icu + SUPPORT llrender_test_checks llcommon_test_checks + DEFINES ${font_defines} ) - al_add_test(${test} GL PROJECT llrender LIBRARIES llrender DEFINES ${font_defines}) - endforeach() - al_add_test( - alfontshaping_gl - GL + + # The GL suites that stand a context up for each test case and end the GL + # classes with it, on LLWindowManager's hidden window: the paths of the + # font libraries above that rasterize, and llimagegl, which also makes a + # context again in the middle of a case. Never beside the shared context + # below, whose classes they would end under it. + al_add_test_binary( + llrender_gl_fresh_tests PROJECT llrender + SOURCES + tests/alfontcolrv1_gl_test.cpp + tests/alfontshaping_gl_test.cpp + tests/llfontbitmapcache_gl_test.cpp + tests/llfontfreetype_gl_test.cpp + tests/llfontregistry_gl_test.cpp + tests/llimagegl_test.cpp LIBRARIES llrender ll::icu + SUPPORT llrender_test_checks llcommon_test_checks DEFINES ${font_defines} + GL + ) + + # The GL suites that share one context for the process, each case putting + # back what it changed of it: + # - fonts drawn, cached and laid out (llfontgl, llfonttextcache), UI images + # and the immediate-mode 2D helpers' quads; + # - the hidden window itself, and a worker thread's shared context on it; + # - tessellation stages and the quad conventions the terrain relies on; + # - the infinite reverse-Z projection: stored depth, round trip and depth + # order out to 100 km; + # - which fragment output reaches a render target's colour attachment, and + # integer attachments cleared and drawn; + # - LLRender's cached clear colour and scissor box, and LLGLSScissor; + # - LLGLSLShader's uniform value cache, the compute path (programs built + # through the shader manager, their images, storage blocks and + # dispatches), integer textures through reserved samplers, what the debug + # validator says of images and stencil reads, and which shader objects a + # program owns and which the manager does; + # - LLRender::drawArraysInstanced: attribute-less instanced geometry, placed + # by the matrices the draw syncs; + # - ALGPUBuffer's store, LLRender's indirect buffer bindings over its + # lifetime, and the readback and stream rings; + # - ALImageSlot: what a unit issues, re-issues and forgets as LLRender keeps + # it, and integer images. + al_add_test_binary( + llrender_gl_shared_tests + PROJECT llrender + SOURCES + tests/algpubuffer_gl_test.cpp + tests/alimageslot_gl_test.cpp + tests/alinfiniteprojection_gl_test.cpp + tests/alinstanceddraw_gl_test.cpp + tests/alrenderstate_gl_test.cpp + tests/altessellation_gl_test.cpp + tests/alwindowsdlheadless_test.cpp + tests/llfontgl_test.cpp + tests/llfonttextcache_test.cpp + tests/llglslshader_gl_test.cpp + tests/llrender2dutils_gl_test.cpp + tests/llrendertarget_gl_test.cpp + tests/lluiimage_test.cpp + LIBRARIES llrender + SUPPORT llrender_test_checks llcommon_test_checks + DEFINES ${font_defines} + GL ) - al_add_test(lluiimage GL PROJECT llrender LIBRARIES llrender) - # The quads the immediate-mode 2D helpers emit. - al_add_test(llrender2dutils_gl GL PROJECT llrender LIBRARIES llrender) - # The hidden window itself, and a worker thread's shared context on it. - al_add_test(alwindowsdlheadless GL PROJECT llrender LIBRARIES llrender) - # Tessellation stages and the quad conventions the terrain relies on. - al_add_test(altessellation_gl GL PROJECT llrender LIBRARIES llrender) - # Infinite reverse-Z projection: stored depth, round trip and depth order out to 100 km. - al_add_test(alinfiniteprojection_gl GL PROJECT llrender LIBRARIES llrender) - # Which fragment output reaches a render target's colour attachment, and integer attachments cleared and drawn. - al_add_test(llrendertarget_gl GL PROJECT llrender LIBRARIES llrender) - # LLRender's cached clear colour and scissor box, and LLGLSScissor. - al_add_test(alrenderstate_gl GL PROJECT llrender LIBRARIES llrender) - # LLGLSLShader's uniform value cache: what a program holds after each kind of set, and across a relink; the - # compute path: programs built through the shader manager, their images, storage blocks and dispatches; integer - # textures through reserved samplers; what the debug validator says of images and stencil reads; and which shader - # objects a program owns and which the manager does. - al_add_test(llglslshader_gl GL PROJECT llrender LIBRARIES llrender) - # LLRender::drawArraysInstanced: attribute-less instanced geometry, placed by the matrices the draw syncs. - al_add_test(alinstanceddraw_gl GL PROJECT llrender LIBRARIES llrender) - # ALGPUBuffer's store, and LLRender's indirect buffer bindings over its lifetime; the readback and stream rings. - al_add_test(algpubuffer_gl GL PROJECT llrender LIBRARIES llrender) - # ALImageSlot: what a unit issues, re-issues and forgets as LLRender keeps it, and integer images. - al_add_test(alimageslot_gl GL PROJECT llrender LIBRARIES llrender) endif() diff --git a/indra/llrender/llfontgl.cpp b/indra/llrender/llfontgl.cpp index f3ae317f7f..d7d269ec1e 100644 --- a/indra/llrender/llfontgl.cpp +++ b/indra/llrender/llfontgl.cpp @@ -1965,21 +1965,23 @@ void LLFontGL::initClass(F32 screen_dpi, F32 x_scale, F32 y_scale, bool create_gl_textures) { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; - sVertDPI = (F32)llfloor(screen_dpi * y_scale); - sHorizDPI = (F32)llfloor(screen_dpi * x_scale); - sScaleX = x_scale; - sScaleY = y_scale; - sAppDir = app_dir; - // Single-shot: bail if the registry was already created via either // overload. Reload-with-explicit-path isn't supported — the test - // caller initializes once. + // caller initializes once. Ignored whole: the fonts already made were + // made at the DPI and scale they have, which a second call must not + // change under them. if (sFontRegistry) { LL_WARNS() << "LLFontGL::initClass(explicit path) called with an existing registry; ignoring" << LL_ENDL; return; } + sVertDPI = (F32)llfloor(screen_dpi * y_scale); + sHorizDPI = (F32)llfloor(screen_dpi * x_scale); + sScaleX = x_scale; + sScaleY = y_scale; + sAppDir = app_dir; + sFontRegistry = new LLFontRegistry(create_gl_textures); sFontRegistry->parseFontInfoFromFile(fonts_xml_path, font_overrides); diff --git a/indra/llrender/llrender.h b/indra/llrender/llrender.h index e9f8c3a06b..cc9e87b226 100644 --- a/indra/llrender/llrender.h +++ b/indra/llrender/llrender.h @@ -62,6 +62,8 @@ class LLRenderTarget; class LLTexture; class LLVertexBufferData; +namespace ll_test { struct RenderProbe; } + #define LL_MATRIX_STACK_DEPTH 32 // AL_NUM_TEXTURE_SLOTS lives in altextureslot.h alongside the units it sizes. @@ -565,6 +567,8 @@ class LLRender private: friend class LLLightState; + // The tests' state guard reads how deep each matrix stack is. + friend struct ll_test::RenderProbe; LLVertexBuffer* bufferfromCache(U32 attribute_mask, U32 count); LLVertexBuffer* genBuffer(U32 attribute_mask, S32 count); diff --git a/indra/llrender/tests/alfontcolrv1_gl_test.cpp b/indra/llrender/tests/alfontcolrv1_gl_test.cpp index 9312abb530..510858dd25 100644 --- a/indra/llrender/tests/alfontcolrv1_gl_test.cpp +++ b/indra/llrender/tests/alfontcolrv1_gl_test.cpp @@ -36,15 +36,13 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut -{ namespace { using namespace ll_test; @@ -62,91 +60,76 @@ namespace ll_test::FontStateScope font_scope; }; - typedef test_group alfontcolrv1_render_test; - typedef alfontcolrv1_render_test::object alfontcolrv1_render_object; - tut::alfontcolrv1_render_test alfontcolrv1_render_testcase("ALFontColrV1Render"); -} // End-to-end: requesting a Color glyph for the fire emoji on // Noto-COLRv1 must route through renderColrV1Glyph → painter → // setSubImageBGRA → Color atlas. Verify the returned info has // Color glyph type and the cache's Color page has a live GL // texture name. - template<> template<> - void alfontcolrv1_render_object::test<1>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: a Color fire emoji is painted onto a live Color atlas page", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - ensure("getGlyphInfo returned an entry for fire emoji", gi != nullptr); - ensure_equals("delivered glyph type is Color (COLRv1 raster)", - (S32)gi->mGlyphType, (S32)EFontGlyphType::Color); - ensure("glyph has positive width", gi->mWidth > 0); - ensure("glyph has positive height", gi->mHeight > 0); + REQUIRE(gi != nullptr); + CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Color); + CHECK(gi->mWidth > 0); + CHECK(gi->mHeight > 0); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("Color atlas allocated at least one sheet", - cache->getNumBitmaps(EFontGlyphType::Color) >= 1u); + REQUIRE(cache != nullptr); + CHECK(cache->getNumBitmaps(EFontGlyphType::Color) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Color, 0); - ensure("Color sheet 0 LLImageGL present", page != nullptr); - ensure("Color sheet 0 has a live GL texture name", - page->getTexName() != 0); + REQUIRE(page != nullptr); + CHECK(page->getTexName() != 0); } // Several COLRv1 emojis in a row share the Color atlas — a single // 1024² Color sheet has plenty of room for many small color // bitmaps. Pins that the painter+atlas integration doesn't // allocate an extra sheet per glyph. - template<> template<> - void alfontcolrv1_render_object::test<2>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: three COLRv1 emoji share one Color atlas sheet", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); const llwchar fire = 0x1F525; const llwchar heart = 0x2764; const llwchar rocket = 0x1F680; - ensure("fire glyph allocated", - ft->getGlyphInfo(fire, EFontGlyphType::Color) != nullptr); - ensure("heart glyph allocated", - ft->getGlyphInfo(heart, EFontGlyphType::Color) != nullptr); - ensure("rocket glyph allocated", - ft->getGlyphInfo(rocket, EFontGlyphType::Color) != nullptr); + REQUIRE(ft->getGlyphInfo(fire, EFontGlyphType::Color) != nullptr); + REQUIRE(ft->getGlyphInfo(heart, EFontGlyphType::Color) != nullptr); + REQUIRE(ft->getGlyphInfo(rocket, EFontGlyphType::Color) != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure_equals("three Color glyphs share a single Color atlas sheet", - cache->getNumBitmaps(EFontGlyphType::Color), 1u); + CHECK(cache->getNumBitmaps(EFontGlyphType::Color) == 1u); } // Repeat lookup is a cache hit, not a re-rasterize. Returning // the same LLFontGlyphInfo pointer pins the head's char_glyph // info cache (so each render call doesn't rebuild the painter // input on every frame). - template<> template<> - void alfontcolrv1_render_object::test<3>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: a repeat Color lookup returns the same cached LLFontGlyphInfo", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); LLFontGlyphInfo* first = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); LLFontGlyphInfo* second = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - ensure("first lookup non-null", first != nullptr); - ensure("second lookup non-null", second != nullptr); - ensure_equals("repeat lookup returns same LLFontGlyphInfo pointer", - first, second); + REQUIRE(first != nullptr); + REQUIRE(second != nullptr); + CHECK(first == second); } // Color atlas generation counter advances on the first emoji @@ -154,24 +137,22 @@ namespace // buffer captured before the rasterize would invalidate. Pins // the global-generation contract that LLFontBitmapCache holds // for COLRv1 atlases too — not just Grayscale. - template<> template<> - void alfontcolrv1_render_object::test<4>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: the first Color raster advances the bitmap cache's generation", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); const S32 gen_before = cache->getCacheGeneration(); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Color); - ensure("glyph allocated", gi != nullptr); + REQUIRE(gi != nullptr); const S32 gen_after = cache->getCacheGeneration(); - ensure("cache generation advanced after Color rasterize", - gen_after > gen_before); + CHECK(gen_after > gen_before); } // (Test 5 removed: pinned LLFontGlyphInfo::mTintWithForeground @@ -183,30 +164,27 @@ namespace // doesn't accumulate state across calls (e.g., a regression that // grew the staging buffer on every call would still produce a // valid bitmap but mWidth/mHeight could drift). - template<> template<> - void alfontcolrv1_render_object::test<6>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: one painter paints the same glyph twice at the same size", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result r1, r2; const LLColor4U fg(255, 255, 255, 255); - ensure("first paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); const S32 w1 = r1.mWidth, h1 = r1.mHeight; - ensure("second paintGlyph (same glyph) succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r2)); - ensure_equals("repeat paint width matches", r2.mWidth, w1); - ensure_equals("repeat paint height matches", r2.mHeight, h1); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r2)); + CHECK(r2.mWidth == w1); + CHECK(r2.mHeight == h1); } // Grayscale-COLRv1 routing: requesting EFontGlyphType::Grayscale on a @@ -215,30 +193,26 @@ namespace // change in renderGlyph drops the bitmap_type==Color restriction, so // Grayscale lookups on COLRv1 faces produce a luminance-shaded mask // in the LA atlas instead of an empty / outline-only glyph. - template<> template<> - void alfontcolrv1_render_object::test<7>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: a Grayscale lookup on a COLRv1 face paints into the Grayscale atlas", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, EFontGlyphType::Grayscale); - ensure("Grayscale lookup returns an entry", gi != nullptr); - ensure_equals("delivered glyph type is Grayscale (LA atlas)", - (S32)gi->mGlyphType, (S32)EFontGlyphType::Grayscale); - ensure("Grayscale glyph has positive width", gi->mWidth > 0); - ensure("Grayscale glyph has positive height", gi->mHeight > 0); + REQUIRE(gi != nullptr); + CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Grayscale); + CHECK(gi->mWidth > 0); + CHECK(gi->mHeight > 0); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("Grayscale atlas allocated at least one sheet", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + REQUIRE(cache != nullptr); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("Grayscale sheet 0 LLImageGL present", page != nullptr); - ensure("Grayscale sheet 0 has a live GL texture name", - page->getTexName() != 0); + REQUIRE(page != nullptr); + CHECK(page->getTexName() != 0); } // Force-monochrome static is wired in at the LLFontGL render-call layer. @@ -247,33 +221,30 @@ namespace // verify that the Grayscale routing works while it's set, which is // what the call-site code does at llfontgl.cpp:560/715/862. Save and // restore the static so the test doesn't perturb sibling tests. - template<> template<> - void alfontcolrv1_render_object::test<8>() + TEST_CASE_METHOD(alfontcolrv1_render_data, "ALFontColrV1Render: with sForceMonochromeEmoji set the emoji is looked up as Grayscale", "[ALFontColrV1Render]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); const bool saved = LLFontGL::sForceMonochromeEmoji; LLFontGL::sForceMonochromeEmoji = true; LLPointer ft = loadFtHead(path); - ensure("Noto-COLRv1 loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); // Simulate the render-call selector at llfontgl.cpp:560 with // use_color=true and force_mono=true → request Grayscale. const EFontGlyphType requested = (!/*use_color*/true || LLFontGL::sForceMonochromeEmoji) ? EFontGlyphType::Grayscale : EFontGlyphType::Color; - ensure_equals("force-mono toggle downgrades Color to Grayscale at " - "the request layer", (S32)requested, - (S32)EFontGlyphType::Grayscale); + UNSCOPED_INFO("force-mono toggle downgrades Color to Grayscale at " + "the request layer"); + CHECK((S32)requested == (S32)EFontGlyphType::Grayscale); LLFontGlyphInfo* gi = ft->getGlyphInfo(0x1F525, requested); - ensure("Grayscale lookup under force-mono returns an entry", - gi != nullptr); - ensure_equals("Grayscale path landed in Grayscale atlas", - (S32)gi->mGlyphType, (S32)EFontGlyphType::Grayscale); + REQUIRE(gi != nullptr); + CHECK((S32)gi->mGlyphType == (S32)EFontGlyphType::Grayscale); LLFontGL::sForceMonochromeEmoji = saved; } diff --git a/indra/llrender/tests/alfontcolrv1_test.cpp b/indra/llrender/tests/alfontcolrv1_test.cpp index ba95c9af03..191907b650 100644 --- a/indra/llrender/tests/alfontcolrv1_test.cpp +++ b/indra/llrender/tests/alfontcolrv1_test.cpp @@ -32,7 +32,7 @@ #include "llfonttest_helpers.h" -#include "../test/lltut.h" +#include #include #include @@ -61,16 +61,13 @@ namespace } } -namespace tut -{ namespace { using namespace ll_test; - // Each test_group<>::object spins up a fresh fixture for every test, - // so init/cleanup the font manager once per test rather than once per - // process. Cheaper than tracking init state and matches the test- - // isolation model TUT expects. + // Each case gets a fresh fixture, so init/cleanup the font manager + // once per test rather than once per process. Cheaper than tracking + // init state and keeps each case isolated from the others. struct alfontcolrv1_data { alfontcolrv1_data() @@ -83,63 +80,55 @@ namespace } }; - typedef test_group alfontcolrv1_test; - typedef alfontcolrv1_test::object alfontcolrv1_object; - tut::alfontcolrv1_test alfontcolrv1_testcase("ALFontColrV1"); -} // Painter input validation: null hb_font fails cleanly without a crash. // Doesn't need a real font, so this test runs even when the test data // dir is empty (e.g., a stripped CI checkout). - template<> template<> - void alfontcolrv1_object::test<1>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: paintGlyph with a null hb_font returns false and an empty result", "[ALFontColrV1]") { ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph(null hb_font) returns false", - !painter.paintGlyph(nullptr, /*glyph_index=*/0, + CHECK_FALSE(painter.paintGlyph(nullptr, /*glyph_index=*/0, /*point_size=*/14.f, fg, /*palette_index=*/0, result)); - ensure("Result.mBitmap stays null on failure", - result.mBitmap == nullptr); - ensure_equals("Result.mWidth stays zero on failure", - result.mWidth, 0); + CHECK(result.mBitmap == nullptr); + CHECK(result.mWidth == 0); } // hasColrV1 is the in-tree probe for "FreeType's FT_LOAD_COLOR can't // rasterise this — route through the paint walker." Verify it fires // for Noto-COLRv1.ttf, which ships a COLRv1 paint table. - template<> template<> - void alfontcolrv1_object::test<2>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: hasColrV1 and hasColor are true for Noto-COLRv1", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("Noto-COLRv1 loaded", face.notNull() && face->isValid()); - ensure("Noto-COLRv1 hasColrV1 == true", face->hasColrV1()); - ensure("Noto-COLRv1 hasColor == true (general flag)", face->hasColor()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + CHECK(face->hasColrV1()); + CHECK(face->hasColor()); } // Negative: InterVariable.woff2 is a plain text font with no COLR // table, so hasColrV1 must stay false. Guards against the probe // false-positiving on any FT_HAS_COLOR-y face. - template<> template<> - void alfontcolrv1_object::test<3>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: hasColrV1 is false for a plain text face with no COLR table", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("Inter loaded", face.notNull() && face->isValid()); - ensure("Inter hasColrV1 == false", !face->hasColrV1()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + CHECK_FALSE(face->hasColrV1()); } // Painter end-to-end: feed a known emoji glyph (fire, U+1F525) through @@ -147,66 +136,62 @@ namespace // one non-zero-alpha pixel. Also exercises the FT_Get_Color_Glyph_ClipBox // path (Noto-COLRv1 ships clip boxes for its glyphs) and the BGRA atlas // packing logic in paintGlyph. - template<> template<> - void alfontcolrv1_object::test<4>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: paintGlyph paints the fire emoji into a BGRA bitmap with some alpha", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); - ensure("face has COLRv1", face->hasColrV1()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); + CHECK(face->hasColrV1()); hb_font_t* hb_font = face->getHbFont(); - ensure("hb_font available", hb_font != nullptr); + REQUIRE(hb_font != nullptr); // Resolve the fire emoji's glyph index via the cmap. Avoids hardcoding // a glyph_id that could shift across font versions. const hb_codepoint_t fire_cp = 0x1F525; hb_codepoint_t fire_gid = 0; - ensure("cmap maps U+1F525 to a glyph", - hb_font_get_nominal_glyph(hb_font, fire_cp, &fire_gid) - && fire_gid != 0); + CHECK(hb_font_get_nominal_glyph(hb_font, fire_cp, &fire_gid)); + CHECK(fire_gid != 0); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); const bool ok = painter.paintGlyph(hb_font, fire_gid, /*point_size=*/14.f, fg, /*palette_index=*/0, result); - ensure("paintGlyph succeeded", ok); - ensure("painter produced bitmap", - result.mBitmap != nullptr - && result.mWidth > 0 && result.mHeight > 0 - && result.mPitch != 0); - ensure("painter drew non-zero alpha somewhere", - anyAlpha(result.mBitmap, result.mWidth, result.mHeight, result.mPitch)); + CHECK(ok); + REQUIRE(result.mBitmap != nullptr); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); + CHECK(result.mPitch != 0); + CHECK(anyAlpha(result.mBitmap, result.mWidth, result.mHeight, result.mPitch)); } // VS-16 cmap absence is the trigger for shape_sub_run's stripping path. // Verify Noto-COLRv1 actually has the shape we expect: ZWJ in cmap but // VS-16 not. Other emoji fonts that ship VS-16 (e.g. Twemoji) take the // no-strip path; the shape pipeline branches on this flag per-face. - template<> template<> - void alfontcolrv1_object::test<5>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: Noto-COLRv1's cmap has ZWJ but no VS-16", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); // ZWJ should resolve to a real glyph (cmap entry present). - ensure("Noto cmap has ZWJ glyph", - face->getCharGlyphIndex(0x200D) != 0u); + CHECK(face->getCharGlyphIndex(0x200D) != 0u); // VS-16 should NOT — shape_sub_run keys VS-16 stripping on this. - ensure_equals("Noto cmap has no VS-16 glyph", - face->getCharGlyphIndex(0xFE0F), 0u); + CHECK(face->getCharGlyphIndex(0xFE0F) == 0u); } // paintGlyph on an out-of-range glyph index (beyond the font's @@ -214,19 +199,19 @@ namespace // hb_font_paint_glyph which returns 0 for unknown glyphs; the // wrapper turns that into a Result with mBitmap=null and // mWidth=0. - template<> template<> - void alfontcolrv1_object::test<6>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: paintGlyph fails cleanly on a glyph index past the font's glyphs", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); hb_font_t* hb_font = face->getHbFont(); - ensure("hb_font available", hb_font != nullptr); + REQUIRE(hb_font != nullptr); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; @@ -237,12 +222,9 @@ namespace const bool ok = painter.paintGlyph(hb_font, /*glyph_index=*/0xFFFFFFFE, 14.f, fg, 0, result); - ensure("paintGlyph on out-of-range glyph index fails cleanly", - !ok); - ensure("result.mBitmap stays null on out-of-range", - result.mBitmap == nullptr); - ensure_equals("result.mWidth zero on out-of-range", - result.mWidth, 0); + CHECK_FALSE(ok); + CHECK(result.mBitmap == nullptr); + CHECK(result.mWidth == 0); } // Painter staging surface reuse: paint two different emoji @@ -250,17 +232,17 @@ namespace // call's result must reflect the second glyph (not the first // — a staging-buffer leak would surface as the second result // pointing at stale first-glyph bytes when dimensions match). - template<> template<> - void alfontcolrv1_object::test<7>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: one painter paints two different emoji one after the other", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); hb_font_t* hb_font = face->getHbFont(); // Two distinct emoji: fire (U+1F525) and red heart (U+2764). @@ -275,19 +257,19 @@ namespace hb_codepoint_t heart = 0; hb_font_get_nominal_glyph(hb_font, 0x2764, &heart); if (fire == 0 || heart == 0) - skip("Noto-COLRv1 missing one of the test emoji codepoints"); + SKIP("Noto-COLRv1 missing one of the test emoji codepoints"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result r1, r2; - ensure("first paintGlyph (fire) succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); - ensure("first painter produced bitmap", - r1.mBitmap != nullptr && r1.mWidth > 0 && r1.mHeight > 0); - - ensure("second paintGlyph (heart) succeeded on same painter", - painter.paintGlyph(hb_font, heart, 14.f, fg, 0, r2)); - ensure("second painter produced bitmap", - r2.mBitmap != nullptr && r2.mWidth > 0 && r2.mHeight > 0); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r1)); + REQUIRE(r1.mBitmap != nullptr); + CHECK(r1.mWidth > 0); + CHECK(r1.mHeight > 0); + + REQUIRE(painter.paintGlyph(hb_font, heart, 14.f, fg, 0, r2)); + REQUIRE(r2.mBitmap != nullptr); + CHECK(r2.mWidth > 0); + CHECK(r2.mHeight > 0); } // 2-em-square surface fallback (88585ff86d): a glyph with no @@ -301,30 +283,29 @@ namespace // also produces a sized bitmap. A negative version (synthetic // glyph forcing the fallback) requires a hand-built TTF and // is left for the integration suite. - template<> template<> - void alfontcolrv1_object::test<8>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: the fire emoji's surface is sized on the order of the point size", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) { - skip("Noto-COLRv1.ttf not present in test data dir"); + SKIP("Noto-COLRv1.ttf not present in test data dir"); } LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); constexpr F32 point_size = 14.f; ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, point_size, fg, 0, result)); + REQUIRE(painter.paintGlyph(hb_font, fire, point_size, fg, 0, result)); // Surface dimensions are on the order of point_size — the // ClipBox / FT-extents path produces a bitmap whose width and // height are within a small multiple of the requested point @@ -332,10 +313,10 @@ namespace // the lower bound; a regression that ballooned to 2× the // 2-em square (32×32 fallback for 14pt) would fail the upper // bound (~3× point_size). - ensure("surface width is in a reasonable range for 14pt", - result.mWidth >= 4 && result.mWidth <= (S32)(point_size * 4.f)); - ensure("surface height is in a reasonable range for 14pt", - result.mHeight >= 4 && result.mHeight <= (S32)(point_size * 4.f)); + CHECK(result.mWidth >= 4); + CHECK(result.mWidth <= (S32)(point_size * 4.f)); + CHECK(result.mHeight >= 4); + CHECK(result.mHeight <= (S32)(point_size * 4.f)); } // (Test 9 removed: pinned the mForegroundOnly flag on the painter @@ -347,19 +328,18 @@ namespace // gradient sampling used the same color for every offset) would // produce a single solid color → 1 distinct triple. Real gradient // emojis show many colors. Pin >= 3. - template<> template<> - void alfontcolrv1_object::test<10>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: the fire emoji's gradients paint at least three distinct colours", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; @@ -368,10 +348,10 @@ namespace // them away. constexpr F32 ps = 32.f; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, ps, fg, 0, result)); - ensure("painter produced bitmap", - result.mBitmap != nullptr && result.mWidth > 0 && result.mHeight > 0); + REQUIRE(painter.paintGlyph(hb_font, fire, ps, fg, 0, result)); + REQUIRE(result.mBitmap != nullptr); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); std::set distinct; // packed BGR (alpha excluded) for (S32 y = 0; y < result.mHeight; ++y) @@ -389,8 +369,7 @@ namespace } // Fire emoji has at minimum a yellow body + orange gradient + // red highlights; >= 3 distinct triples is a conservative bound. - ensure("fire emoji produces >= 3 distinct RGB triples (gradient)", - distinct.size() >= 3); + CHECK(distinct.size() >= 3); } // palette_index argument propagates to hb_font_paint_glyph: paint @@ -399,31 +378,30 @@ namespace // differ at non-zero-alpha positions. Skip if only one palette is // present (no observable difference, but the code path is exercised // either way). - template<> template<> - void alfontcolrv1_object::test<11>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: palette_index reaches hb_font_paint_glyph: palette 1 paints differently", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); const LLColor4U fg(255, 255, 255, 255); ALFontColrV1Painter painter_a, painter_b; ALFontColrV1Painter::Result r0, r1; - ensure("palette=0 paintGlyph succeeded", - painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0, r0)); - ensure("palette=1 paintGlyph succeeded", - painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/1, r1)); - ensure("both produced bitmaps", - r0.mBitmap && r1.mBitmap - && r0.mWidth == r1.mWidth && r0.mHeight == r1.mHeight); + REQUIRE(painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0, r0)); + REQUIRE(painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/1, r1)); + REQUIRE(r0.mBitmap); + REQUIRE(r1.mBitmap); + // The comparison below walks r1 at r0's size. + REQUIRE(r0.mWidth == r1.mWidth); + REQUIRE(r0.mHeight == r1.mHeight); // Compare bytes — if the font has only one palette, hb resolves // both calls against palette 0 and the bitmaps match. Skip in @@ -445,44 +423,39 @@ namespace } } if (!any_diff) - skip("Noto-COLRv1 ships only one CPAL palette"); + SKIP("Noto-COLRv1 ships only one CPAL palette"); - ensure("palette swap changed at least one painted pixel", any_diff); + CHECK(any_diff); } // OutputFormat::Gray basics: format selector hits the gray fold path // and returns a single-channel buffer with mPitch == width and // non-zero coverage somewhere in the bitmap. Pins the §1/§2 contract // that Gray output is single-channel, top-row first, tightly packed. - template<> template<> - void alfontcolrv1_object::test<12>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: OutputFormat::Gray paints a tightly packed one-channel bitmap", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result result; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph(format=Gray) succeeded", - painter.paintGlyph(hb_font, fire, /*point_size=*/14.f, fg, + REQUIRE(painter.paintGlyph(hb_font, fire, /*point_size=*/14.f, fg, /*palette=*/0, ALFontColrV1Painter::OutputFormat::Gray, result)); - ensure_equals("Result.mFormat == Gray", - (S32)result.mFormat, - (S32)ALFontColrV1Painter::OutputFormat::Gray); - ensure("Gray output has positive dims", - result.mWidth > 0 && result.mHeight > 0); - ensure_equals("Gray pitch is width (tightly packed single channel)", - result.mPitch, result.mWidth); + CHECK((S32)result.mFormat == (S32)ALFontColrV1Painter::OutputFormat::Gray); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); + CHECK(result.mPitch == result.mWidth); bool any_nonzero = false; for (S32 y = 0; y < result.mHeight && !any_nonzero; ++y) @@ -493,8 +466,7 @@ namespace if (row[x] != 0) { any_nonzero = true; break; } } } - ensure("Gray output has at least one non-zero coverage pixel", - any_nonzero); + CHECK(any_nonzero); } // Gray-fold formula pin: paint the same glyph in BGRA and Gray and @@ -505,25 +477,23 @@ namespace // also verifies the foreground-collapses-to-alpha invariant since // Y == A and Y > A*0.15 holds, so the test is a strict superset of // the "fg-only collapses to alpha" case. - template<> template<> - void alfontcolrv1_object::test<13>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: each Gray byte is the hybrid luminance fold of the BGRA paint", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result rgba_r, gray_r; const LLColor4U fg(255, 255, 255, 255); - ensure("BGRA paint succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, rgba_r)); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, rgba_r)); // Stash the BGRA bytes — the next paintGlyph call invalidates // mBitmap (it points back into mStaging which we overwrite below). std::vector bgra_bytes(rgba_r.mBitmap, @@ -531,12 +501,12 @@ namespace const S32 bgra_w = rgba_r.mWidth, bgra_h = rgba_r.mHeight; const S32 bgra_pitch = rgba_r.mPitch; - ensure("Gray paint succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, ALFontColrV1Painter::OutputFormat::Gray, gray_r)); - ensure_equals("Gray and BGRA produce same width", gray_r.mWidth, bgra_w); - ensure_equals("Gray and BGRA produce same height", gray_r.mHeight, bgra_h); + // The comparison below walks the Gray bitmap at the BGRA size. + REQUIRE(gray_r.mWidth == bgra_w); + REQUIRE(gray_r.mHeight == bgra_h); S32 mismatches = 0; for (S32 y = 0; y < bgra_h; ++y) @@ -559,8 +529,7 @@ namespace ++mismatches; } } - ensure_equals("every Gray byte matches the hybrid fold formula", - mismatches, 0); + CHECK(mismatches == 0); } // Hybrid floor pin: scan the BGRA result for fully-saturated palette @@ -570,12 +539,11 @@ namespace // still holds, just isn't observable here. The point of the test is // to lock the constant against future tweaks that would silently // crush dark CPAL details. - template<> template<> - void alfontcolrv1_object::test<14>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: Gray keeps an alpha floor under saturated black palette pixels", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); @@ -620,49 +588,45 @@ namespace if (r == 0 && g == 0 && b == 0 && a > 200) { const U32 expected_floor = (a * 38u) >> 8; - ensure("Gray byte at saturated-black palette pixel " - "≥ alpha*floor (no crush to zero)", - (U32)gray_row[x] + 1u >= expected_floor); + CAPTURE(cp, x, y); + REQUIRE((U32)gray_row[x] + 1u >= expected_floor); return; } } } } - skip("no fully-saturated-black palette pixel found across test glyphs"); + SKIP("no fully-saturated-black palette pixel found across test glyphs"); } // Out-of-range palette_index clamps to 0 deterministically. Paint // fire with palette=0 and palette=99; the resulting bitmaps must // be byte-identical (the painter clamps any oversized index to 0 // before calling hb_font_paint_glyph). - template<> template<> - void alfontcolrv1_object::test<15>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: an out-of-range palette_index paints byte for byte as palette 0", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter_a, painter_b; ALFontColrV1Painter::Result r_zero, r_huge; const LLColor4U fg(255, 255, 255, 255); - ensure("palette=0 paint succeeded", - painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0u, r_zero)); - ensure("palette=99 paint succeeded (clamps to 0)", - painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/99u, r_huge)); - ensure_equals("clamped result has same width", r_huge.mWidth, r_zero.mWidth); - ensure_equals("clamped result has same height", r_huge.mHeight, r_zero.mHeight); - ensure_equals("clamped result has same pitch", r_huge.mPitch, r_zero.mPitch); + REQUIRE(painter_a.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/0u, r_zero)); + REQUIRE(painter_b.paintGlyph(hb_font, fire, 14.f, fg, /*palette=*/99u, r_huge)); + CHECK(r_huge.mWidth == r_zero.mWidth); + // The memcmp below reads r_huge for r_zero's pitch times height. + REQUIRE(r_huge.mHeight == r_zero.mHeight); + REQUIRE(r_huge.mPitch == r_zero.mPitch); const size_t bytes = (size_t)r_zero.mPitch * r_zero.mHeight; - ensure("clamped result is byte-identical to palette=0", - std::memcmp(r_zero.mBitmap, r_huge.mBitmap, bytes) == 0); + CHECK(std::memcmp(r_zero.mBitmap, r_huge.mBitmap, bytes) == 0); } // Fallback point_size kicks in when the hb_font has ppem unset. @@ -671,26 +635,26 @@ namespace // bitmap rather than returning false. Specifically targets the // 2-em fallback inside the empty-bbox branch — without the // fallback point_size override, that branch would return false. - template<> template<> - void alfontcolrv1_object::test<16>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: with ppem unset paintGlyph sizes from the fallback point size or fails", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); // Build a dedicated hb_font on top of the same FT_Face so we can // mutate its ppem without affecting the face cache shared with // other tests in this group. hb_font_t* shared = face->getHbFont(); - ensure("shared hb_font available", shared != nullptr); + REQUIRE(shared != nullptr); hb_face_t* hb_face = hb_font_get_face(shared); hb_font_t* hb_font = hb_font_create(hb_face); - ensure("derived hb_font allocated", hb_font != nullptr); + REQUIRE(hb_font != nullptr); hb_font_set_ppem(hb_font, 0, 0); unsigned px = 1, py = 1; hb_font_get_ppem(hb_font, &px, &py); - ensure("ppem really is unset on derived font", px == 0 && py == 0); + CHECK(px == 0); + CHECK(py == 0); // Pick a glyph by index — without a scale set, the cmap query // through hb_font_get_nominal_glyph may not work as expected, so @@ -701,7 +665,7 @@ namespace if (fire == 0) { hb_font_destroy(hb_font); - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); } ALFontColrV1Painter painter; @@ -720,8 +684,8 @@ namespace // failure mode we're protecting against. if (ok) { - ensure("fallback path produced a sized bitmap", - result.mWidth > 0 && result.mHeight > 0); + CHECK(result.mWidth > 0); + CHECK(result.mHeight > 0); } hb_font_destroy(hb_font); } @@ -732,43 +696,45 @@ namespace // post-transform paint commands. A regression that drops PAD back to // 1 (or below) would surface here as a fringe of non-zero pixels at // the buffer boundary that visually clips the glyph at the atlas. - template<> template<> - void alfontcolrv1_object::test<17>() + TEST_CASE_METHOD(alfontcolrv1_data, "ALFontColrV1: a painted glyph's outermost rows and columns have zero alpha", "[ALFontColrV1]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); hb_font_t* hb_font = face->getHbFont(); hb_codepoint_t fire = 0; hb_font_get_nominal_glyph(hb_font, 0x1F525, &fire); if (fire == 0) - skip("Noto-COLRv1 has no fire emoji"); + SKIP("Noto-COLRv1 has no fire emoji"); ALFontColrV1Painter painter; ALFontColrV1Painter::Result r; const LLColor4U fg(255, 255, 255, 255); - ensure("paintGlyph succeeded", - painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r)); - ensure("bitmap valid", - r.mBitmap && r.mWidth >= 4 && r.mHeight >= 4); + REQUIRE(painter.paintGlyph(hb_font, fire, 14.f, fg, 0, r)); + REQUIRE(r.mBitmap); + // The loops below read row mHeight-1 and column mWidth-1. + REQUIRE(r.mWidth >= 4); + REQUIRE(r.mHeight >= 4); // First row, last row. for (S32 x = 0; x < r.mWidth; ++x) { const U8* top = r.mBitmap + x*4; const U8* bottom = r.mBitmap + (ptrdiff_t)(r.mHeight-1)*r.mPitch + x*4; - ensure_equals("top row alpha is zero", (S32)top[3], 0); - ensure_equals("bottom row alpha is zero", (S32)bottom[3], 0); + CAPTURE(x); + REQUIRE((S32)top[3] == 0); + REQUIRE((S32)bottom[3] == 0); } // First column, last column. for (S32 y = 0; y < r.mHeight; ++y) { const U8* left = r.mBitmap + (ptrdiff_t)y*r.mPitch + 0*4; const U8* right = r.mBitmap + (ptrdiff_t)y*r.mPitch + (r.mWidth-1)*4; - ensure_equals("left col alpha is zero", (S32)left[3], 0); - ensure_equals("right col alpha is zero", (S32)right[3], 0); + CAPTURE(y); + REQUIRE((S32)left[3] == 0); + REQUIRE((S32)right[3] == 0); } } } diff --git a/indra/llrender/tests/alfontshaping_gl_test.cpp b/indra/llrender/tests/alfontshaping_gl_test.cpp index 5d9bd95e1e..379af02f3b 100644 --- a/indra/llrender/tests/alfontshaping_gl_test.cpp +++ b/indra/llrender/tests/alfontshaping_gl_test.cpp @@ -33,7 +33,7 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include @@ -41,15 +41,13 @@ #include #include -namespace tut -{ namespace { using namespace ll_test; // GL-backed group: monospace shaping ends up rendering glyphs through // getGlyphInfoByIndex → renderAndCreateGlyph → atlas → gGL.bind on - // the test 3 (kerning) path. Wrapped in a separate fixture that + // the kerning path (the GPOS case below). Wrapped in a separate fixture that // pulls in the headless GL context so the rasterizer can // satisfy the bind. struct alfontshaping_gl_data @@ -58,10 +56,6 @@ namespace ll_test::FontStateScope font_scope; }; - typedef test_group alfontshaping_gl_test; - typedef alfontshaping_gl_test::object alfontshaping_gl_object; - tut::alfontshaping_gl_test alfontshaping_gl_testcase("ALFontShapingGL"); -} // Strict-monospace (ligatures off): shape "AB" through DejaVuSansMono. // Routes through HB with the kFixedWidthStrict feature plan @@ -69,65 +63,55 @@ namespace // produces one glyph per codepoint with bit-exact FT mXAdvance and // zero positioning offsets — same contract the retired bypass // enforced. Pins the cell-alignment invariant. - template<> template<> - void alfontshaping_gl_object::test<1>() + TEST_CASE_METHOD(alfontshaping_gl_data, "ALFontShapingGL: strict monospace shapes AB to equal FT cell advances and no offsets", "[ALFontShapingGL]") { const std::string path = std::string(kFontDir) + "DejaVuSansMono.woff2"; if (!fileExists(path)) - skip("DejaVuSansMono.woff2 not present"); + SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSansMono loaded", ft.notNull()); - ensure("DejaVuSansMono is fixed-width", ft->isFixedWidth()); - ensure("monospace ligatures default off", - !ft->getAllowMonospaceLigatures()); + REQUIRE(ft.notNull()); + CHECK(ft->isFixedWidth()); + CHECK_FALSE(ft->getAllowMonospaceLigatures()); const Text s = text('A','B'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("AB produces 2 glyphs through HB strict-mono path", - out.size(), 2u); - ensure("each shaped glyph has positive advance", - out[0].x_advance > 0.f && out[1].x_advance > 0.f); - ensure_equals("strict-mono: A and B advances are equal", - out[0].x_advance, out[1].x_advance); + REQUIRE(out.size() == 2u); + CHECK(out[0].x_advance > 0.f); + CHECK(out[1].x_advance > 0.f); + CHECK(out[0].x_advance == out[1].x_advance); // HB output for monospace ASCII matches FT mXAdvance bit-exact // under the strict feature plan (verified by the long-line - // probe in test 20). - ensure_equals("strict-mono glyph[0] advance == ft->getXAdvance('A')", - out[0].x_advance, ft->getXAdvance(L'A')); + // cell-alignment probe in alfontshaping_test.cpp). + CHECK(out[0].x_advance == ft->getXAdvance(L'A')); // Strict-mono path also forces zero positioning offsets, since // monospace ASCII triggers no GPOS adjustments under the // feature plan (kern, mark, mkmk all suppressed for ASCII). - ensure_equals("strict-mono glyph[0] x_offset is zero", - out[0].x_offset, 0.f); - ensure_equals("strict-mono glyph[0] y_offset is zero", - out[0].y_offset, 0.f); + CHECK(out[0].x_offset == 0.f); + CHECK(out[0].y_offset == 0.f); } // Programmer-mono opt-in via setAllowMonospaceLigatures(true). // Routes through HB with the kFixedWidthLigaturesOk feature plan // (kern off, ligatures allowed). Cell alignment invariant on the // pre-ligation columns still holds. - template<> template<> - void alfontshaping_gl_object::test<2>() + TEST_CASE_METHOD(alfontshaping_gl_data, "ALFontShapingGL: monospace with ligatures allowed keeps AV on equal cell advances", "[ALFontShapingGL]") { const std::string path = std::string(kFontDir) + "DejaVuSansMono.woff2"; if (!fileExists(path)) - skip("DejaVuSansMono.woff2 not present"); + SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSansMono loaded", ft.notNull()); + REQUIRE(ft.notNull()); ft->setAllowMonospaceLigatures(true); - ensure("ligatures-on toggle applied", - ft->getAllowMonospaceLigatures()); + CHECK(ft->getAllowMonospaceLigatures()); const Text s = text('A','V'); // AV is a classic kerned pair std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("AV produces 2 glyphs through HB", out.size(), 2u); - ensure("each shaped glyph has positive advance", - out[0].x_advance > 0.f && out[1].x_advance > 0.f); - ensure_equals("HB-monospace-with-ligatures: A and V advances are equal", - out[0].x_advance, out[1].x_advance); + REQUIRE(out.size() == 2u); + CHECK(out[0].x_advance > 0.f); + CHECK(out[1].x_advance > 0.f); + CHECK(out[0].x_advance == out[1].x_advance); } // HB GPOS plumbing: shape "AV" (classic Latin kerned pair) through @@ -137,14 +121,13 @@ namespace // advances. A regression that disabled GPOS unconditionally would // make these equal. Skip if no Latin kern pair fires (test // verifies nothing then). - template<> template<> - void alfontshaping_gl_object::test<3>() + TEST_CASE_METHOD(alfontshaping_gl_data, "ALFontShapingGL: GPOS kerning reaches HB shaping for some Latin pair in DejaVuSans", "[ALFontShapingGL]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); // Try a handful of classic kerned pairs; pick the first that // shows a measurable kern. DejaVu's GPOS coverage varies. @@ -177,9 +160,8 @@ namespace } } if (!found_kerned) - skip("DejaVuSans lacks a measurable Latin kern pair"); + SKIP("DejaVuSans lacks a measurable Latin kern pair"); - ensure("at least one Latin kern pair fired through HB GPOS", - found_kerned); + CHECK(found_kerned); } } diff --git a/indra/llrender/tests/alfontshaping_test.cpp b/indra/llrender/tests/alfontshaping_test.cpp index 5852663895..7d7a336870 100644 --- a/indra/llrender/tests/alfontshaping_test.cpp +++ b/indra/llrender/tests/alfontshaping_test.cpp @@ -37,7 +37,7 @@ #include "llfonttest_helpers.h" -#include "../test/lltut.h" +#include #include #include @@ -60,8 +60,6 @@ // stem-darkening test below can FT_Property_Get against it. extern FT_Library gFTLibrary; -namespace tut -{ namespace { using namespace ll_test; @@ -85,37 +83,36 @@ namespace } }; - typedef test_group alfontshaping_test; - typedef alfontshaping_test::object alfontshaping_object; - tut::alfontshaping_test alfontshaping_testcase("ALFontShaping"); -} // Null root face and empty/inverted ranges all produce empty // output. The header documents these as the safe-fallback // contracts callers rely on for "fall back to 1:1 codepoint path." - template<> template<> - void alfontshaping_object::test<1>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a null face or an empty or inverted or overlong slice shapes nothing", "[ALFontShaping]") { std::vector out; const Text s = text('a','b','c'); ALFontShaping::shapeRun(/*root_face=*/nullptr, s, 0, s.size(), out); - ensure("null root face -> empty output", out.empty()); + UNSCOPED_INFO("null root face"); + CHECK(out.empty()); const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); ALFontShaping::shapeRun(ft, s, /*begin=*/2, /*end=*/2, out); - ensure("begin == end -> empty output", out.empty()); + UNSCOPED_INFO("begin == end"); + CHECK(out.empty()); ALFontShaping::shapeRun(ft, s, /*begin=*/3, /*end=*/2, out); - ensure("begin > end -> empty output", out.empty()); + UNSCOPED_INFO("begin > end"); + CHECK(out.empty()); ALFontShaping::shapeRun(ft, s, /*begin=*/0, /*end=*/99, out); - ensure("end > size -> empty output", out.empty()); + UNSCOPED_INFO("end > size"); + CHECK(out.empty()); } // ASCII through a proportional Latin font: 1 glyph per codepoint, @@ -130,66 +127,58 @@ namespace // → gGL.bind, which a pure-CPU test binary can't satisfy. The cmap // match below is the strongest identity check we can make without // rasterizing. - template<> template<> - void alfontshaping_object::test<2>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: ASCII shapes to one glyph per codepoint with its cmap id and an advance", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); const Text s = text('a','b','c'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("3 glyphs for 'abc'", out.size(), 3u); + REQUIRE(out.size() == 3u); for (const auto& g : out) { - ensure("each glyph has a face", g.face != nullptr); - ensure("each glyph has a glyph_id", g.glyph_id != 0u); - ensure("each glyph has a positive x_advance", - g.x_advance > 0.f); + REQUIRE(g.face != nullptr); + REQUIRE(g.glyph_id != 0u); + REQUIRE(g.x_advance > 0.f); } // Identity check: the shape's glyph_id for each ASCII char // must match the face's cmap-direct lookup. A regression that // bound HB to the wrong face would produce ids that disagree. - ensure_equals("glyph[0] id matches cmap('a')", - out[0].glyph_id, ft->getCharGlyphIndex(L'a')); - ensure_equals("glyph[1] id matches cmap('b')", - out[1].glyph_id, ft->getCharGlyphIndex(L'b')); - ensure_equals("glyph[2] id matches cmap('c')", - out[2].glyph_id, ft->getCharGlyphIndex(L'c')); + CHECK(out[0].glyph_id == ft->getCharGlyphIndex(L'a')); + CHECK(out[1].glyph_id == ft->getCharGlyphIndex(L'b')); + CHECK(out[2].glyph_id == ft->getCharGlyphIndex(L'c')); } // shapeRun rebases cluster indices to the original wstr; // shapeLine returns slice-local clusters. Pin both contracts — // the renderer hot path branches on this distinction // (alfontshaping.h:80-90). - template<> template<> - void alfontshaping_object::test<3>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: shapeRun clusters index the whole string and shapeLine's the slice", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); const Text s = text('X','Y','a','b','c'); const size_t begin = s.at(2); const size_t end = s.at(5); std::vector shape_run_out; ALFontShaping::shapeRun(ft, s, begin, end, shape_run_out); - ensure_equals("shapeRun produced 3 glyphs", shape_run_out.size(), 3u); + REQUIRE(shape_run_out.size() == 3u); // 'a' starts where the slice does. - ensure_equals("shapeRun cluster[0] is an original-string offset", - shape_run_out[0].cluster, (S32)begin); + CHECK(shape_run_out[0].cluster == (S32)begin); const auto& shape_line_out = ALFontShaping::shapeLine(ft, s, begin, end); - ensure_equals("shapeLine produced 3 glyphs", shape_line_out.size(), 3u); + REQUIRE(shape_line_out.size() == 3u); // shapeLine clusters are slice-local: 'a' is at slice offset 0. - ensure_equals("shapeLine cluster[0] is slice-local offset 0", - shape_line_out[0].cluster, 0); + CHECK(shape_line_out[0].cluster == 0); } // Cache hit contract: shaping the same (slice, root_face) twice @@ -197,14 +186,13 @@ namespace // This is what callers depend on across frames (pen positions and // glyph ids must not drift), and it's the load-bearing invariant // for the LRU's reuse semantics. - template<> template<> - void alfontshaping_object::test<4>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: shaping the same slice twice gives a bit-identical glyph stream", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); const Text s = text('h','e','l','l','o'); std::vector first; @@ -212,16 +200,13 @@ namespace std::vector second; ALFontShaping::shapeRun(ft, s, 0, s.size(), second); - ensure_equals("cache hit produces same glyph count", - second.size(), first.size()); + REQUIRE(second.size() == first.size()); for (size_t i = 0; i < first.size(); ++i) { - ensure_equals("cache hit: same glyph_id", - second[i].glyph_id, first[i].glyph_id); - ensure_equals("cache hit: same cluster", - second[i].cluster, first[i].cluster); - ensure_equals("cache hit: same x_advance", - second[i].x_advance, first[i].x_advance); + CAPTURE(i); + REQUIRE(second[i].glyph_id == first[i].glyph_id); + REQUIRE(second[i].cluster == first[i].cluster); + REQUIRE(second[i].x_advance == first[i].x_advance); } } @@ -234,30 +219,30 @@ namespace // Without instrumentation we observe "cache cleared" indirectly // by checking the returned reference identity from shapeLine — // a cleared entry rebuilds to a fresh storage location. - template<> template<> - void alfontshaping_object::test<5>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: clearCacheForFace on one face leaves another face's entry in place", "[ALFontShaping]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + InterVariable both required"); + SKIP("DejaVuSans + InterVariable both required"); LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - ensure("both faces loaded", a.notNull() && b.notNull()); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); const Text s = text('a','b'); const auto& a_first = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_first = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure("a shape non-empty", !a_first.empty()); - ensure("b shape non-empty", !b_first.empty()); + REQUIRE_FALSE(a_first.empty()); + REQUIRE_FALSE(b_first.empty()); const void* a_addr0 = a_first.data(); const void* b_addr0 = b_first.data(); // Hit A again: cache hit returns same storage address. const auto& a_second = ALFontShaping::shapeLine(a, s, 0, s.size()); - ensure_equals("A cache hit: same backing storage", - a_second.data(), a_addr0); + UNSCOPED_INFO("A cache hit: same backing storage"); + CHECK(a_second.data() == a_addr0); ALFontShaping::clearCacheForFace(a); @@ -266,53 +251,49 @@ namespace // address by the allocator after clear is implementation- // defined and not part of the contract.) const auto& a_after = ALFontShaping::shapeLine(a, s, 0, s.size()); - ensure("A re-shape produces output", !a_after.empty()); + CHECK_FALSE(a_after.empty()); const auto& b_after = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure_equals("B cache survived clearCacheForFace(A)", - b_after.data(), b_addr0); + UNSCOPED_INFO("B cache survived clearCacheForFace(A)"); + CHECK(b_after.data() == b_addr0); } // ZWJ heart-on-fire: U+2764 U+200D U+1F525 must collapse to a // single glyph through Noto-COLRv1 (whose GSUB has the ligature // rule). Pins d4a5a4c904 — the regression that left ZWJ sequences // unligatured because the run was fragmenting at VS-16 and ZWJ. - template<> template<> - void alfontshaping_object::test<6>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: the ZWJ heart-on-fire ligates to one glyph through Noto-COLRv1", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + REQUIRE(ft.notNull()); // U+2764 (heart) U+200D (ZWJ) U+1F525 (fire) const Text s = text(0x2764, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("heart-on-fire shaped to non-empty output", !out.empty()); - ensure_equals("heart-on-fire ZWJ ligated to a single glyph", - out.size(), 1u); + REQUIRE_FALSE(out.empty()); + CHECK(out.size() == 1u); } // VS-16 strip path: shaping U+2764 U+FE0F through Noto-COLRv1 // (whose cmap lacks U+FE0F) must produce the same output as // shaping U+2764 alone — shape_sub_run pre-strips VS-16 before // passing to HB. Pins alfontshaping.cpp:257-264. - template<> template<> - void alfontshaping_object::test<7>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a VS-16 the cmap lacks is stripped: same glyphs as the base alone", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + REQUIRE(ft.notNull()); // Sanity: confirm VS-16 is genuinely absent from this font's // cmap (otherwise the strip path doesn't fire and the test // verifies nothing useful). - ensure_equals("Noto-COLRv1 cmap lacks U+FE0F", - ft->getCharGlyphIndex(0xFE0F), 0u); + CHECK(ft->getCharGlyphIndex(0xFE0F) == 0u); const Text with_vs = text(0x2764, 0xFE0F); const Text without_vs = text(0x2764); @@ -322,34 +303,85 @@ namespace ALFontShaping::shapeRun(ft, with_vs, 0, with_vs.size(), with_out); ALFontShaping::shapeRun(ft, without_vs, 0, without_vs.size(), without_out); - ensure_equals("with-VS and without-VS produce same glyph count", - with_out.size(), without_out.size()); - ensure("with-VS produced at least one glyph", !with_out.empty()); + REQUIRE(with_out.size() == without_out.size()); + REQUIRE_FALSE(with_out.empty()); for (size_t i = 0; i < with_out.size(); ++i) { - ensure_equals("with-VS glyph_id matches without-VS", - with_out[i].glyph_id, without_out[i].glyph_id); + CAPTURE(i); + REQUIRE(with_out[i].glyph_id == without_out[i].glyph_id); } } + // clearCache empties the LRU; subsequent shapes repopulate it. The + // earlier version of this test compared the returned reference's + // data() pointer before vs after clear and asserted the address + // changed — that was allocator-implementation-defined. Modern + // allocators (LFH on Windows, jemalloc, mimalloc, ...) recycle freed + // slots LIFO, so the rebuilt entry's vector storage routinely lands + // at the just-freed address and the assertion flaked ~3-of-5 runs. + // cacheSize() pins the contract the cache actually guarantees + // (count goes to 0 then back to 1) without depending on heap + // recycling behavior. + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: clearCache empties the LRU; subsequent shapes repopulate it", "[ALFontShaping]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + LLPointer ft = loadFt(path); + REQUIRE(ft.notNull()); + + const Text s = text('h','i'); + const auto& first = ALFontShaping::shapeLine(ft, s, 0, s.size()); + REQUIRE_FALSE(first.empty()); + UNSCOPED_INFO("first shape leaves one entry"); + CHECK(ALFontShaping::cacheSize() == 1u); + // Snapshot the size BEFORE clearCache invalidates `first` — + // shapeLine returns a reference into the LRU, so the entry's + // storage is gone the moment the LRU is cleared. Reading + // first.size() afterward would be a use-after-free. + const size_t first_size = first.size(); + const void* addr0 = first.data(); + + // Cache hit: same storage AND the entry count stays at one. + const auto& second = ALFontShaping::shapeLine(ft, s, 0, s.size()); + UNSCOPED_INFO("cache hit storage is stable"); + CHECK(second.data() == addr0); + UNSCOPED_INFO("cache hit doesn't add an entry"); + CHECK(ALFontShaping::cacheSize() == 1u); + + ALFontShaping::clearCache(); + CHECK(ALFontShaping::cacheSize() == 0u); + + // Miss: re-shape repopulates the cache. Output content is + // identical (shaping is deterministic given the same face + + // codepoints); the cache state is what changed. + const auto& after = ALFontShaping::shapeLine(ft, s, 0, s.size()); + REQUIRE_FALSE(after.empty()); + UNSCOPED_INFO("post-clear miss repopulated to one entry"); + CHECK(ALFontShaping::cacheSize() == 1u); + UNSCOPED_INFO("post-clear shape has as many glyphs as the first"); + CHECK(after.size() == first_size); + } + // Multi-hop shape run: ASCII + CJK + Emoji through a head (DejaVu) // with two fallbacks (SourceHanSans + Noto-COLRv1). shape_sub_run // itemizes by face coverage so each cluster routes to the right // face. Pins gap audit #3 — multi-hop selectShapingFace was // untested, the existing fallback tests use a single hop only. - template<> template<> - void alfontshaping_object::test<9>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: ASCII then CJK then emoji route to the head and each of two fallbacks", "[ALFontShaping]") { const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string han = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(dejavu) || !fileExists(han) || !fileExists(emoji)) - skip("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); LLPointer head = loadFt(dejavu); LLPointer cjk = loadFt(han); LLPointer emo = loadFt(emoji); - ensure("all loaded", head.notNull() && cjk.notNull() && emo.notNull()); + REQUIRE(head.notNull()); + REQUIRE(cjk.notNull()); + REQUIRE(emo.notNull()); head->addFallbackFont(cjk); head->addFallbackFont(emo); @@ -357,16 +389,19 @@ namespace const Text s = text('a', 0x4F60, 0x1F525); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure_equals("3 glyphs (one per codepoint)", out.size(), 3u); + REQUIRE(out.size() == 3u); // Each glyph carries the face it came from — verify the routing. - ensure_equals("'a' routed to head (DejaVu)", out[0].face, head.get()); - ensure_equals("CJK routed to SourceHanSans", out[1].face, cjk.get()); - ensure_equals("emoji routed to Noto-COLRv1", out[2].face, emo.get()); + CHECK(out[0].face == head.get()); + CHECK(out[1].face == cjk.get()); + CHECK(out[2].face == emo.get()); // All three glyphs should have non-zero ids (no notdef anywhere). - ensure("'a' resolved to non-zero glyph", out[0].glyph_id != 0u); - ensure("CJK resolved to non-zero glyph", out[1].glyph_id != 0u); - ensure("emoji resolved to non-zero glyph", out[2].glyph_id != 0u); + UNSCOPED_INFO("'a' resolved to non-zero glyph"); + CHECK(out[0].glyph_id != 0u); + UNSCOPED_INFO("CJK resolved to non-zero glyph"); + CHECK(out[1].glyph_id != 0u); + UNSCOPED_INFO("emoji resolved to non-zero glyph"); + CHECK(out[2].glyph_id != 0u); } // VS-16 strip preserves cluster identity in the original wstr. @@ -375,14 +410,13 @@ namespace // emoji at position 3 stays cluster=3, even though it was at // position 2 in the post-strip buffer fed to HB). Pins // cluster_back_map rebase at alfontshaping.cpp:392-403. - template<> template<> - void alfontshaping_object::test<10>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: after a VS-16 strip clusters still index the original string", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + REQUIRE(ft.notNull()); // Heart + VS-16 + ZWJ + fire. The second codepoint (FE0F) gets // stripped. The output's clusters must still reference the original @@ -391,7 +425,7 @@ namespace const Text s = text(0x2764, 0xFE0F, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("output non-empty", !out.empty()); + REQUIRE_FALSE(out.empty()); // Whatever the final shape (ZWJ ligature collapses to 1 glyph, // or 2 with VS-16 stripped), every cluster must point into the // ORIGINAL string. A regression that left clusters in the post-strip @@ -400,10 +434,10 @@ namespace // the strip MUST rebase past it. for (const auto& g : out) { - ensure("cluster points into original string range", - g.cluster >= 0 && g.cluster < (S32)s.size()); - ensure_not_equals("cluster does NOT point at stripped VS-16 position", - g.cluster, (S32)s.at(1)); + REQUIRE(g.cluster >= 0); + REQUIRE(g.cluster < (S32)s.size()); + UNSCOPED_INFO("cluster does NOT point at stripped VS-16 position"); + REQUIRE(g.cluster != (S32)s.at(1)); } } @@ -412,17 +446,17 @@ namespace // dropped by clearCacheForFace(A). The shape LRU is keyed on root // face only (alfontshaping.cpp:62-64); B's own entries (if any) // survive — we don't double-purge across heads sharing fallbacks. - template<> template<> - void alfontshaping_object::test<11>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: clearCacheForFace on a head keeps the entry of its fallback face", "[ALFontShaping]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + SourceHanSans required"); + SKIP("DejaVuSans + SourceHanSans required"); LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - ensure("both loaded", a.notNull() && b.notNull()); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); a->addFallbackFont(b); // Shape via A — its entry's glyph stream contains a B glyph @@ -430,8 +464,8 @@ namespace const Text s = text('a', 0x4F60); const auto& a_first = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_first = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure("a non-empty", !a_first.empty()); - ensure("b non-empty", !b_first.empty()); + REQUIRE_FALSE(a_first.empty()); + REQUIRE_FALSE(b_first.empty()); const void* a_addr = a_first.data(); const void* b_addr = b_first.data(); @@ -443,14 +477,16 @@ namespace // not load-bearing for the contract; B's survival is.) const auto& a_after = ALFontShaping::shapeLine(a, s, 0, s.size()); const auto& b_after = ALFontShaping::shapeLine(b, s, 0, s.size()); - ensure("A re-shape produces output", !a_after.empty()); - ensure_equals("B's entry survived clearCacheForFace(A)", - b_after.data(), b_addr); + CHECK_FALSE(a_after.empty()); + // b_first is a reference into the cache: it dangles unless B's + // entry survived, so this REQUIRE guards the read below. + UNSCOPED_INFO("B's entry survived clearCacheForFace(A)"); + REQUIRE(b_after.data() == b_addr); // Sanity: the data we read out of the survived B entry is the // same B stream we observed before — a hidden cross-purge would // have reset it. - ensure_equals("B output unchanged across A's cache flush", - b_after.size(), b_first.size()); + UNSCOPED_INFO("B output unchanged across A's cache flush"); + CHECK(b_after.size() == b_first.size()); } // notdef contract: shaping a codepoint through a head with NO @@ -458,75 +494,21 @@ namespace // glyph_id == 0). The renderer relies on this to draw the .notdef // rectangle for missing-glyph indication; an empty output would // silently drop the codepoint instead. - template<> template<> - void alfontshaping_object::test<12>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a codepoint no face covers shapes to one notdef glyph", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); // U+1F525 (fire emoji) is not in DejaVuSans's cmap, and we // attached no fallback, so HB must produce notdef. const Text s = text(0x1F525); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("uncovered codepoint produces 1 glyph", out.size(), 1u); - ensure_equals("uncovered codepoint glyph_id == 0 (notdef)", - out[0].glyph_id, 0u); - } - - // clearCache empties the LRU; subsequent shapes repopulate it. The - // earlier version of this test compared the returned reference's - // data() pointer before vs after clear and asserted the address - // changed — that was allocator-implementation-defined. Modern - // allocators (LFH on Windows, jemalloc, mimalloc, ...) recycle freed - // slots LIFO, so the rebuilt entry's vector storage routinely lands - // at the just-freed address and the assertion flaked ~3-of-5 runs. - // cacheSize() pins the contract the cache actually guarantees - // (count goes to 0 then back to 1) without depending on heap - // recycling behavior. - template<> template<> - void alfontshaping_object::test<8>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); - - const Text s = text('h','i'); - const auto& first = ALFontShaping::shapeLine(ft, s, 0, s.size()); - ensure("shape produced output", !first.empty()); - ensure_equals("first shape leaves one entry", - ALFontShaping::cacheSize(), 1u); - // Snapshot the size BEFORE clearCache invalidates `first` — - // shapeLine returns a reference into the LRU, so the entry's - // storage is gone the moment the LRU is cleared. Reading - // first.size() afterward would be a use-after-free. - const size_t first_size = first.size(); - const void* addr0 = first.data(); - - // Cache hit: same storage AND the entry count stays at one. - const auto& second = ALFontShaping::shapeLine(ft, s, 0, s.size()); - ensure_equals("cache hit storage is stable", second.data(), addr0); - ensure_equals("cache hit doesn't add an entry", - ALFontShaping::cacheSize(), 1u); - - ALFontShaping::clearCache(); - ensure_equals("clearCache empties the LRU", - ALFontShaping::cacheSize(), 0u); - - // Miss: re-shape repopulates the cache. Output content is - // identical (shaping is deterministic given the same face + - // codepoints); the cache state is what changed. - const auto& after = ALFontShaping::shapeLine(ft, s, 0, s.size()); - ensure("post-clear rebuild produced output", !after.empty()); - ensure_equals("post-clear miss repopulated to one entry", - ALFontShaping::cacheSize(), 1u); - ensure_equals("post-clear shape is content-identical", - after.size(), first_size); + REQUIRE(out.size() == 1u); + CHECK(out[0].glyph_id == 0u); } // Keycap cluster across face boundary: '9' + U+FE0F + U+20E3 must @@ -536,23 +518,23 @@ namespace // fragment off onto Noto-COLRv1 — producing a bare '9' followed by a // standalone keycap mark with no base. Pins the keycap fix in // shape_all_sub_runs (alfontshaping.cpp:497-546). - template<> template<> - void alfontshaping_object::test<13>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: keycap 9 goes whole to the emoji fallback and composes to one glyph", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); const Text s = text('9', 0xFE0F, 0x20E3); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("keycap shaped to non-empty output", !out.empty()); + REQUIRE_FALSE(out.empty()); // The emoji face has the GSUB rule digit + U+20E3 -> keycap glyph // (VS-16 stripped pre-shape). All output glyphs must come from // the emoji face — not head — to confirm the cluster routed as @@ -560,16 +542,14 @@ namespace // '9' glyph on `head` and only the keycap mark on `emoji`. for (const auto& g : out) { - ensure_equals("every keycap-cluster glyph routes to emoji face", - g.face, emoji.get()); + REQUIRE(g.face == emoji.get()); } // Strong pin: the GSUB rule actually fires across the face // boundary. A weaker form of the regression — routing matches // but GSUB doesn't collapse — would leave 2+ glyphs on the // emoji face (the '9' outline plus an unbound keycap mark), // matching exactly what the user sees in the buggy report. - ensure_equals("keycap '9️⃣' GSUB-collapsed to a single glyph", - out.size(), 1u); + CHECK(out.size() == 1u); } // ZWJ heart-on-fire across face boundary: U+2764 (BMP heart, present @@ -577,42 +557,95 @@ namespace // emoji face). Phase 1 routes the whole cluster to the emoji face so // its GSUB can collapse the ZWJ ligature. A regression to per-cp // itemization would split the heart onto head, breaking the rule. - template<> template<> - void alfontshaping_object::test<14>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: the ZWJ heart-on-fire goes whole to the emoji fallback and ligates", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); // Sanity: head DOES carry U+2764 — selectShapingFace would // happily route it to head without the cluster fast path. - ensure("head face has U+2764 in cmap (without Phase 1 the heart " - "would route to head and split the cluster)", - head->getCharGlyphIndex(0x2764) != 0u); + UNSCOPED_INFO("head face has U+2764 in cmap (without Phase 1 the heart " + "would route to head and split the cluster)"); + CHECK(head->getCharGlyphIndex(0x2764) != 0u); const Text s = text(0x2764, 0xFE0F, 0x200D, 0x1F525); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("heart-on-fire shaped to non-empty output", !out.empty()); + REQUIRE_FALSE(out.empty()); for (const auto& g : out) { - ensure_equals("every heart-on-fire glyph routes to emoji face", - g.face, emoji.get()); + REQUIRE(g.face == emoji.get()); } // Pin the ligature too — Noto-COLRv1's GSUB collapses the - // sequence to a single glyph. Test 6 covers the same rule when - // shaping directly through Noto; this asserts it still fires when - // the head is a non-emoji font and the cluster is routed across - // the fallback boundary. - ensure_equals("heart-on-fire ZWJ ligated to a single glyph " - "even across the fallback boundary", - out.size(), 1u); + // sequence to a single glyph. The first heart-on-fire case covers + // the same rule when shaping directly through Noto; this asserts + // it still fires when the head is a non-emoji font and the + // cluster is routed across the fallback boundary. + CHECK(out.size() == 1u); + } + + // Bare digit control: '9' alone (no FE0F + 20E3 follower) is NOT a + // cluster per the walker. Phase 1's fast path must not interfere — + // the digit routes to head as before, one glyph, no surprises. + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a bare 9 is no emoji cluster and stays on the head face", "[ALFontShaping]") + { + const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(head_path) || !fileExists(emoji_path)) + SKIP("DejaVuSans + Noto-COLRv1 required"); + + LLPointer head = loadFt(head_path); + LLPointer emoji = loadFt(emoji_path); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); + head->addFallbackFont(emoji); + + const Text s = text('9'); + std::vector out; + ALFontShaping::shapeRun(head, s, 0, s.size(), out); + REQUIRE(out.size() == 1u); + CHECK(out[0].face == head.get()); + CHECK(out[0].glyph_id == head->getCharGlyphIndex(L'9')); + } + + // Hash-keycap directly through the emoji face: pin that Noto-COLRv1 + // actually has the GSUB rule for '#' + VS-16 + U+20E3 -> composed + // keycap (one glyph). The '9' rule is exercised by the keycap 9 case + // across a face boundary; this version uses Noto-COLRv1 directly so a font- + // level missing rule is observable as glyph_count > 1 (not a routing + // bug). User report: '#️⃣' renders as '#' + a standalone keycap mark + // — diagnosed by this test as either a font-rule miss or a routing + // miss, with the existing rule-fires path through 9️⃣ as the control. + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: keycap # composes to one glyph through Noto-COLRv1 alone", "[ALFontShaping]") + { + const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(path)) + SKIP("Noto-COLRv1.ttf not present"); + LLPointer ft = loadFt(path); + REQUIRE(ft.notNull()); + + // Sanity: the font carries '#' and U+20E3 in cmap. If either + // were missing, hb_shape would emit notdef before any GSUB + // could match. + CHECK(ft->getCharGlyphIndex(L'#') != 0u); + CHECK(ft->getCharGlyphIndex(0x20E3) != 0u); + + const Text s = text('#', 0xFE0F, 0x20E3); + std::vector out; + ALFontShaping::shapeRun(ft, s, 0, s.size(), out); + REQUIRE_FALSE(out.empty()); + // If the GSUB keycap rule fires, the run collapses to a single + // composed glyph (the same shape the heart-on-fire and keycap 9 + // cases expect for their respective ligatures). + CHECK(out.size() == 1u); } // Adjacent clusters: '3️⃣' (keycap) + '🏳️‍🌈' (pride flag, ZWJ @@ -623,17 +656,17 @@ namespace // down to a bare white flag (the rainbow part of the ZWJ ligature // collapsed against the wrong context). Each cluster must shape as // its own sub-run, so each rule sees only its own codepoints. - template<> template<> - void alfontshaping_object::test<17>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: adjacent keycap and pride-flag clusters each shape on their own", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); // '3' VS-16 U+20E3 (keycap), then U+1F3F3 (waving white flag) @@ -642,19 +675,18 @@ namespace 0x1F3F3, 0xFE0F, 0x200D, 0x1F308); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("adjacent clusters shaped to non-empty output", !out.empty()); + REQUIRE_FALSE(out.empty()); for (const auto& g : out) { - ensure_equals("every glyph routes to emoji face", - g.face, emoji.get()); + REQUIRE(g.face == emoji.get()); } // Both clusters compose: keycap '3️⃣' -> 1 glyph, pride flag // '🏳️‍🌈' -> 1 glyph. Total 2. A regression that merged the // two clusters into one buffer would produce a different count // (e.g. the pride flag's GSUB matching against the keycap mark // and falling back to bare white-flag + standalone rainbow). - ensure_equals("each adjacent cluster collapses independently", - out.size(), 2u); + UNSCOPED_INFO("each adjacent cluster collapses independently"); + CHECK(out.size() == 2u); } // Cluster with no emoji-face coverage: route to root rather than to @@ -665,31 +697,30 @@ namespace // We model this by attaching DejaVu (which has '#' but lacks U+20E3) // as the only fallback and shaping a keycap. Result must keep '#' // visible on root rather than tofu-ing the base on a non-covering face. - template<> template<> - void alfontshaping_object::test<18>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a keycap no face can compose keeps its base # on the root face", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "InterVariable.woff2"; const std::string fallback_path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(head_path) || !fileExists(fallback_path)) - skip("InterVariable + DejaVuSans required"); + SKIP("InterVariable + DejaVuSans required"); LLPointer head = loadFt(head_path); LLPointer fallback = loadFt(fallback_path); - ensure("both loaded", head.notNull() && fallback.notNull()); + REQUIRE(head.notNull()); + REQUIRE(fallback.notNull()); head->addFallbackFont(fallback); // Sanity: neither face has U+20E3 (keycap mark) — that's the // scenario this test exercises. Neither has the keycap GSUB // rule. Best-case outcome: the base '#' renders through root. - ensure_equals("Inter lacks U+20E3", head->getCharGlyphIndex(0x20E3), 0u); - ensure_equals("DejaVu lacks U+20E3", fallback->getCharGlyphIndex(0x20E3), 0u); - ensure_not_equals("Inter has '#' in cmap", - head->getCharGlyphIndex(L'#'), 0u); + CHECK(head->getCharGlyphIndex(0x20E3) == 0u); + CHECK(fallback->getCharGlyphIndex(0x20E3) == 0u); + CHECK(head->getCharGlyphIndex(L'#') != 0u); const Text s = text('#', 0xFE0F, 0x20E3); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("shape produced output", !out.empty()); + REQUIRE_FALSE(out.empty()); // The base '#' must end up on a face that has it (root). A // regression that routed to a non-covering face would tofu // the '#' (glyph_id == 0) which is the worse failure mode. @@ -699,70 +730,8 @@ namespace if (g.face == head.get() && g.glyph_id == head->getCharGlyphIndex(L'#')) found_hash = true; } - ensure("'#' base routed to a face that has it", - found_hash); - } - - // Hash-keycap directly through the emoji face: pin that Noto-COLRv1 - // actually has the GSUB rule for '#' + VS-16 + U+20E3 -> composed - // keycap (one glyph). The '9' rule is exercised by test 13 across - // a face boundary; this version uses Noto-COLRv1 directly so a font- - // level missing rule is observable as glyph_count > 1 (not a routing - // bug). User report: '#️⃣' renders as '#' + a standalone keycap mark - // — diagnosed by this test as either a font-rule miss or a routing - // miss, with the existing rule-fires path through 9️⃣ as the control. - template<> template<> - void alfontshaping_object::test<16>() - { - const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); - LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); - - // Sanity: the font carries '#' and U+20E3 in cmap. If either - // were missing, hb_shape would emit notdef before any GSUB - // could match. - ensure_not_equals("Noto-COLRv1 has '#' in cmap", - ft->getCharGlyphIndex(L'#'), 0u); - ensure_not_equals("Noto-COLRv1 has U+20E3 in cmap", - ft->getCharGlyphIndex(0x20E3), 0u); - - const Text s = text('#', 0xFE0F, 0x20E3); - std::vector out; - ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("hash-keycap shaped to non-empty output", !out.empty()); - // If the GSUB keycap rule fires, the run collapses to a single - // composed glyph (the same shape test 6 / test 13 expect for - // their respective ligatures). - ensure_equals("hash-keycap '#️⃣' GSUB-collapsed to a single glyph", - out.size(), 1u); - } - - // Bare digit control: '9' alone (no FE0F + 20E3 follower) is NOT a - // cluster per the walker. Phase 1's fast path must not interfere — - // the digit routes to head as before, one glyph, no surprises. - template<> template<> - void alfontshaping_object::test<15>() - { - const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); - - LLPointer head = loadFt(head_path); - LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); - head->addFallbackFont(emoji); - - const Text s = text('9'); - std::vector out; - ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure_equals("bare '9' produces one glyph", out.size(), 1u); - ensure_equals("bare '9' routes to head (not emoji fallback)", - out[0].face, head.get()); - ensure_equals("bare '9' glyph_id matches head's cmap", - out[0].glyph_id, head->getCharGlyphIndex(L'9')); + UNSCOPED_INFO("'#' base routed to a face that has it"); + CHECK(found_hash); } // Subdivision flag: U+1F3F4 (black flag) + tag chars 'gbeng' + the @@ -772,17 +741,17 @@ namespace // to a face whose cmap covers the tag chars. A regression that // dropped tag-char support from the predicate would split the flag // off from its tag bytes and render the bytes as visible glyphs. - template<> template<> - void alfontshaping_object::test<19>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a tag-sequence subdivision flag goes whole to the emoji fallback", "[ALFontShaping]") { const std::string head_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(head_path) || !fileExists(emoji_path)) - skip("DejaVuSans + Noto-COLRv1 required"); + SKIP("DejaVuSans + Noto-COLRv1 required"); LLPointer head = loadFt(head_path); LLPointer emoji = loadFt(emoji_path); - ensure("both loaded", head.notNull() && emoji.notNull()); + REQUIRE(head.notNull()); + REQUIRE(emoji.notNull()); head->addFallbackFont(emoji); // 🏴 (U+1F3F4) + 'g' 'b' 'e' 'n' 'g' tag chars + U+E007F. @@ -791,23 +760,22 @@ namespace 0xE007F); std::vector out; ALFontShaping::shapeRun(head, s, 0, s.size(), out); - ensure("subdivision flag shaped to non-empty output", !out.empty()); + REQUIRE_FALSE(out.empty()); // Routing assertion is the load-bearing one: every cluster // member must land on the emoji face. A predicate regression // that excluded tag chars would split the flag onto emoji and // the tag bytes onto root. for (const auto& g : out) { - ensure_equals("every tag-flag glyph routes to emoji face", - g.face, emoji.get()); + REQUIRE(g.face == emoji.get()); } // Stretch goal: if Noto-COLRv1 carries the GB-ENG subdivision // flag, GSUB collapses the whole sequence to one glyph. Some // Noto-COLRv1 builds ship without subdivision flags — tolerate // up to N glyphs, but never MORE than the input length (which // would mean glyph duplication). - ensure("output glyph count never exceeds input codepoint count", - out.size() <= s.size()); + UNSCOPED_INFO("output glyph count never exceeds input codepoint count"); + CHECK(out.size() <= s.size()); } // Cell-alignment probe. For each monospace face shipped in @@ -820,16 +788,15 @@ namespace // long lines. If it ever does, the strict feature plan in // shape_sub_run needs more entries (and the long-form fix is a // cluster-aware advance snap on HB output). - template<> template<> - void alfontshaping_object::test<20>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: monospace HB shaping keeps 200 glyphs on the cell with no drift", "[ALFontShaping]") { const char* mono_files[] = { "DejaVuSansMono.woff2", "SourceCodeVF-Upright.woff2", }; - // Mixed ASCII pool to exercise more lookups than test 2's - // simple AV pair: alphanumerics, punctuation, brackets, + // Mixed ASCII pool to exercise more lookups than the simple AV + // pair in alfontshaping_gl_test.cpp: alphanumerics, punctuation, brackets, // symbols. Repeated to 200 chars so per-glyph drift // accumulates measurably. const std::string ascii_pool = @@ -850,24 +817,24 @@ namespace const std::string path = std::string(kFontDir) + fname; if (!fileExists(path)) continue; + CAPTURE(fname); LLPointer ft = loadFt(path); - ensure("monospace font loaded", ft.notNull()); - ensure("font is fixed-width", ft->isFixedWidth()); + REQUIRE(ft.notNull()); + REQUIRE(ft->isFixedWidth()); ft->setAllowMonospaceLigatures(true); // route through HB std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("N codepoints produce N glyphs through HB", - out.size(), N); + REQUIRE(out.size() == N); // Reference: first glyph's advance. In a fixed-width face // all ASCII codepoints share one FT mXAdvance, so out[0] // sets the canonical cell. Deliberately don't use // ft->getXAdvance — that path asserts !mIsFallback and // the probe loads as fallback to skip GL pre-warm. - ensure("at least one glyph", !out.empty()); + REQUIRE_FALSE(out.empty()); const F32 ref_advance = out[0].x_advance; - ensure("reference advance positive", ref_advance > 0.f); + REQUIRE(ref_advance > 0.f); F32 max_dev = 0.f; F32 min_adv = ref_advance; @@ -901,17 +868,17 @@ namespace // Tolerance: 0.5px is generous — meaningful column drift // shows as ~1px by char ~50 in production reports. - ensure("per-glyph advance drift < 0.5px", max_dev < 0.5f); - ensure("cumulative advance drift < 0.5px", cumulative_dev < 0.5f); + REQUIRE(max_dev < 0.5f); + REQUIRE(cumulative_dev < 0.5f); // Bypass writes 0 offsets; HB output should match for // ASCII (no GPOS positioning adjustments fire on monospace // ASCII with kern off). - ensure("HB ASCII x_offset is zero", max_xoff < 0.5f); - ensure("HB ASCII y_offset is zero", max_yoff < 0.5f); + REQUIRE(max_xoff < 0.5f); + REQUIRE(max_yoff < 0.5f); ++fonts_tested; } if (fonts_tested == 0) - skip("no monospace fonts present in test data dir"); + SKIP("no monospace fonts present in test data dir"); } // Strict-mono never collapses codepoints into ligated glyphs. @@ -929,8 +896,7 @@ namespace // reasons: (1) catches a future shipped-font upgrade that DOES // include programmer ligatures, and (2) catches an HB upstream // change that fires more lookups by default. - template<> template<> - void alfontshaping_object::test<21>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: strict monospace never ligates: one glyph per codepoint", "[ALFontShaping]") { const char* mono_files[] = { "DejaVuSansMono.woff2", @@ -950,24 +916,23 @@ namespace const std::string path = std::string(kFontDir) + fname; if (!fileExists(path)) continue; + CAPTURE(fname); LLPointer ft = loadFt(path); - ensure("monospace font loaded", ft.notNull()); - ensure("font is fixed-width", ft->isFixedWidth()); + REQUIRE(ft.notNull()); + REQUIRE(ft->isFixedWidth()); // Default: ligatures off. Routes through HB with kFixedWidthStrict. - ensure("ligatures default off", - !ft->getAllowMonospaceLigatures()); + REQUIRE_FALSE(ft->getAllowMonospaceLigatures()); // All ASCII, so a byte is a codepoint here too. const std::string& s = ligature_bait; std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("strict-mono glyph_count == codepoint_count", - out.size(), s.size()); + REQUIRE(out.size() == s.size()); ++fonts_tested; } if (fonts_tested == 0) - skip("no monospace fonts present in test data dir"); + SKIP("no monospace fonts present in test data dir"); } // Combining marks position via GPOS in monospace shaping. The retired @@ -981,15 +946,14 @@ namespace // cluster index, the second has zero x_advance (mark consumes no // cell), and the mark has non-zero positioning (offset OR cluster // composition through ccmp into a precomposed 'á'). - template<> template<> - void alfontshaping_object::test<22>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a combining acute on monospace composes or sits as a zero-advance mark", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSansMono.woff2"; if (!fileExists(path)) - skip("DejaVuSansMono.woff2 not present"); + SKIP("DejaVuSansMono.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSansMono loaded", ft.notNull()); - ensure("DejaVuSansMono is fixed-width", ft->isFixedWidth()); + REQUIRE(ft.notNull()); + CHECK(ft->isFixedWidth()); const Text s = text(L'a', 0x0301); // a + COMBINING ACUTE ACCENT std::vector out; @@ -1003,7 +967,7 @@ namespace k, out[k].glyph_id, out[k].cluster, out[k].x_advance, out[k].x_offset, out[k].y_offset); } - ensure("combining cluster produces at least 1 glyph", !out.empty()); + REQUIRE_FALSE(out.empty()); // Expected outcomes: // (a) ccmp composed to precomposed 'á' → 1 glyph, full cell advance. // (b) Two glyphs (a + acute) in one HB cluster → first carries @@ -1012,28 +976,32 @@ namespace // Either is correct GPOS-driven behavior; the bypass produced // neither (it emitted 2 glyphs, both with full advance). const F32 ref_advance = out[0].x_advance; - ensure("base glyph has positive advance", ref_advance > 0.f); + CHECK(ref_advance > 0.f); if (out.size() == 1) { // ccmp composition path. One cell, no second glyph. - ensure_equals("cluster index is 0 (base)", out[0].cluster, 0); + UNSCOPED_INFO("cluster index is 0 (base)"); + CHECK(out[0].cluster == 0); } else if (out.size() == 2) { // Mark cluster path. Marks share the base's cluster index // and consume zero advance (a hard requirement — a non-zero // mark advance would shift the next character right). - ensure_equals("mark shares base cluster", out[1].cluster, out[0].cluster); - ensure_equals("mark glyph has zero advance", out[1].x_advance, 0.f); + UNSCOPED_INFO("mark shares base cluster"); + CHECK(out[1].cluster == out[0].cluster); + UNSCOPED_INFO("mark glyph has zero advance"); + CHECK(out[1].x_advance == 0.f); } else { - ensure("combining cluster produced unexpected glyph count", false); + UNSCOPED_INFO("combining cluster produced unexpected glyph count"); + CHECK(false); } } // ----------------------------------------------------------------- - // FT/HB consistency block (tests 23-31). Locks the integration + // FT/HB consistency block (the next eight cases). Locks the integration // invariants between FreeType and HarfBuzz so a refactor that breaks // size, scale, glyph identity, hinting, variation axes, cmap // selection, or stem-darkening surfaces immediately. Every test @@ -1060,19 +1028,18 @@ namespace // hhea, ignoring USE_TYPO_METRICS), while FT's size->metrics // ascender honors USE_TYPO_METRICS — so they differ by design. // hb_ft_font_create_referenced leaves ppem at 0 (only scale is set). - template<> template<> - void alfontshaping_object::test<23>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB's font scale is what hb-ft derives from the FT face's size", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); - ensure("face accessible", face != nullptr); + REQUIRE(face != nullptr); hb_font_t* hbf = face->getHbFont(); - ensure("hb_font accessible", hbf != nullptr); + REQUIRE(hbf != nullptr); int hb_x_scale = 0, hb_y_scale = 0; hb_font_get_scale(hbf, &hb_x_scale, &hb_y_scale); @@ -1083,20 +1050,17 @@ namespace // avoid 32-bit overflow at large UPEM × scale products. const int expected_x = (int)((((std::uint64_t)m.x_scale * (std::uint64_t)ftf->units_per_EM) + (1u << 15)) >> 16); const int expected_y = (int)((((std::uint64_t)m.y_scale * (std::uint64_t)ftf->units_per_EM) + (1u << 15)) >> 16); - ensure_equals("HB x_scale matches hb-ft derivation from FT state", - hb_x_scale, expected_x); - ensure_equals("HB y_scale matches hb-ft derivation from FT state", - hb_y_scale, expected_y); + CHECK(hb_x_scale == expected_x); + CHECK(hb_y_scale == expected_y); // Sanity: scale must be positive (zero would mean uninitialized). - ensure("HB x_scale is positive", hb_x_scale > 0); - ensure("HB y_scale is positive", hb_y_scale > 0); + CHECK(hb_x_scale > 0); + CHECK(hb_y_scale > 0); } - // (test<24> intentionally absent — formerly checked hb_font_get_ppem + // (Audit test 24 intentionally absent — it checked hb_font_get_ppem // against FT ppem, but hb_ft_font_create_referenced doesn't set - // ppem on the HB font; only scale carries the size invariant. - // Keeping the gap so the audit's test numbering (23 + 25..31) - // matches the plan file unchanged.) + // ppem on the HB font; only scale carries the size invariant. This + // block is the audit's tests 23 and 25..31, in that order.) // For an unligatured / unkerned ASCII run, HB's per-glyph // x_advance must equal FT's slot->advance.x for the same glyph @@ -1104,14 +1068,13 @@ namespace // outline scaling or load-flag plumbing. DejaVuSans has no GPOS // kern between adjacent lowercase letters, so HB's GPOS pass is // a no-op and per-glyph advances come straight from FT. - template<> template<> - void alfontshaping_object::test<25>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB advances equal FreeType's for an unkerned ASCII run", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); @@ -1120,19 +1083,18 @@ namespace const Text s = text('a','b','c','d','e','f'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("6 glyphs for 'abcdef'", out.size(), 6u); + REQUIRE(out.size() == 6u); for (size_t i = 0; i < out.size(); ++i) { - ensure_equals("FT_Load_Glyph succeeded", - FT_Load_Glyph(ftf, out[i].glyph_id, load_flags), 0); + CAPTURE(i); + REQUIRE(FT_Load_Glyph(ftf, out[i].glyph_id, load_flags) == 0); const F32 ft_advance = ftf->glyph->advance.x * (1.f / 64.f); // 26.6 -> float; equality in float at this precision // would be exact when no GPOS adjustment fired. Tolerate // 1/64 px to cover HB's internal rounding paths. const F32 delta = std::fabs(out[i].x_advance - ft_advance); - ensure("HB x_advance matches FT advance for unkerned glyph", - delta < (1.f / 64.f) + 1e-5f); + REQUIRE(delta < (1.f / 64.f) + 1e-5f); } } @@ -1141,27 +1103,24 @@ namespace // refactor that splits the casts asymmetrically breaks shaped vs // codepoint advance consistency. hb_ft_font_get_load_flags has // existed since HB 1.7 — the codebase requires newer than that. - template<> template<> - void alfontshaping_object::test<26>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB's load flags are the hinting FT renders glyphs with", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); hb_font_t* hbf = face->getHbFont(); - ensure("hb_font accessible", hbf != nullptr); + REQUIRE(hbf != nullptr); const int hb_flags = hb_ft_font_get_load_flags(hbf); const int expected = static_cast(face->hinting()); - ensure_equals("HB load flags == (int)mHinting", - hb_flags, expected); + CHECK(hb_flags == expected); // And confirm the cast equivalence between the two surfaces // (HB takes int, FT takes FT_Int32) hasn't drifted. - ensure_equals("(int)mHinting == (FT_Int32)mHinting", - static_cast(face->hinting()), + CHECK(static_cast(face->hinting()) == (int)static_cast(face->hinting())); } @@ -1170,12 +1129,11 @@ namespace // font's default axis values (typically wght=400) even when FT // renders varied outlines, breaking variation-aware kerning on // fonts like Inter at non-default weights. - template<> template<> - void alfontshaping_object::test<27>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HB shapes at the variation design coordinates FT renders at", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); // Load with wght=600 so setVariationAxis sets a non-default // wght. opsz is auto-set from point_size whenever opsz_set is // false (load() handles both axes via the matching *_set @@ -1183,8 +1141,8 @@ namespace ALFontVarAxes va; va.wght = 600.f; va.wght_set = true; LLPointer ft = new LLFontFreetype; - ensure("InterVariable loaded at weight=600", - ft->loadFace(path, /*point_size=*/14.f, + // A failed load leaves no face for the reads below. + REQUIRE(ft->loadFace(path, /*point_size=*/14.f, /*vert_dpi=*/96.f, /*horz_dpi=*/96.f, /*is_fallback=*/true, /*face_n=*/0, EFontHinting::DEFAULT, @@ -1193,32 +1151,31 @@ namespace const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); hb_font_t* hbf = face->getHbFont(); - ensure("hb_font accessible", hbf != nullptr); + REQUIRE(hbf != nullptr); FT_MM_Var* mm = nullptr; - ensure_equals("FT_Get_MM_Var on a variable font", - FT_Get_MM_Var(ftf, &mm), 0); - ensure("FT_MM_Var allocated", mm != nullptr); + CHECK(FT_Get_MM_Var(ftf, &mm) == 0); + REQUIRE(mm != nullptr); const FT_UInt num_axis = mm->num_axis; - ensure("variable font reports >= 1 axis", num_axis >= 1u); + CHECK(num_axis >= 1u); std::vector ft_coords(num_axis); - ensure_equals("FT_Get_Var_Design_Coordinates", - FT_Get_Var_Design_Coordinates(ftf, num_axis, ft_coords.data()), 0); + CHECK(FT_Get_Var_Design_Coordinates(ftf, num_axis, ft_coords.data()) == 0); unsigned int hb_len = 0; const float* hb_coords = hb_font_get_var_coords_design(hbf, &hb_len); - ensure("HB has design coords set", hb_coords != nullptr); - ensure_equals("HB axis count matches FT", hb_len, (unsigned int)num_axis); + REQUIRE(hb_coords != nullptr); + // The loop below reads hb_coords[i] for every FT axis. + REQUIRE(hb_len == (unsigned int)num_axis); for (FT_UInt i = 0; i < num_axis; ++i) { + CAPTURE(i); const float ft_design = ft_coords[i] / 65536.0f; // 16.16 -> design const float delta = std::fabs(hb_coords[i] - ft_design); // 16.16 round-trip: equality should be exact, but allow // 1/65536 to cover any internal float-conversion rounding. - ensure("HB design coord matches FT design coord", - delta < (1.f / 65536.f) + 1e-6f); + REQUIRE(delta < (1.f / 65536.f) + 1e-6f); } FT_Done_MM_Var(gFTLibrary, mm); @@ -1230,10 +1187,9 @@ namespace // 2.7+, but cff/type1 default ON; without explicit Property_Set on // those, a CFF font with EFontHinting::DEFAULT picks up unwanted // stem darkening. - template<> template<> - void alfontshaping_object::test<28>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: stem darkening is off on every FreeType module that has the property", "[ALFontShaping]") { - ensure("gFTLibrary initialized", gFTLibrary != nullptr); + REQUIRE(gFTLibrary != nullptr); const char* modules[] = { "autofitter", "cff", "type1", "t1cid" }; for (const char* mod : modules) @@ -1247,8 +1203,8 @@ namespace // a no-op. if (err != 0) continue; - ensure(std::string("no-stem-darkening enabled on ") + mod, - no_darken != 0); + CAPTURE(mod); + REQUIRE(no_darken != 0); } } @@ -1256,41 +1212,37 @@ namespace // Unicode font — the resulting active charmap encoding should be // FT_ENCODING_UNICODE so all FT_Get_Char_Index lookups operate on // the Unicode cmap subtable, not whichever subtable FT auto-picked. - template<> template<> - void alfontshaping_object::test<29>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a Unicode font's active FreeType charmap is the Unicode one", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); const ALFontFace* face = ft->getFontFace(); FT_Face ftf = face->face(); - ensure("active charmap is set", ftf->charmap != nullptr); - ensure_equals("active charmap encoding is FT_ENCODING_UNICODE", - (int)ftf->charmap->encoding, - (int)FT_ENCODING_UNICODE); + REQUIRE(ftf->charmap != nullptr); + CHECK((int)ftf->charmap->encoding == (int)FT_ENCODING_UNICODE); } // VS-15 strip parity with VS-16: a face that lacks U+FE0E in cmap // must still produce identical shape output for `` // vs just `` — the do_shape strip pass should drop VS-15 // before HB so the notdef glyph for VS-15 doesn't sit between - // base and follow-up codepoints. Mirrors test<7>'s VS-16 proof. - template<> template<> - void alfontshaping_object::test<30>() + // base and follow-up codepoints. Mirrors the VS-16 strip case's proof. + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a VS-15 the cmap lacks is stripped as VS-16 is", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(path)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); LLPointer ft = loadFt(path); - ensure("Noto-COLRv1 loaded", ft.notNull()); + REQUIRE(ft.notNull()); // Sanity: the test only verifies anything if Noto-COLRv1 also // lacks VS-15. Color emoji fonts typically don't ship VS-15. if (ft->getCharGlyphIndex(0xFE0E) != 0u) - skip("Noto-COLRv1 unexpectedly has U+FE0E in cmap"); + SKIP("Noto-COLRv1 unexpectedly has U+FE0E in cmap"); const Text with_vs = text(0x2764, 0xFE0E); const Text without_vs = text(0x2764); @@ -1300,29 +1252,27 @@ namespace ALFontShaping::shapeRun(ft, with_vs, 0, with_vs.size(), with_out); ALFontShaping::shapeRun(ft, without_vs, 0, without_vs.size(), without_out); - ensure_equals("with-VS15 and without-VS15 produce same glyph count", - with_out.size(), without_out.size()); - ensure("with-VS15 produced at least one glyph", !with_out.empty()); + REQUIRE(with_out.size() == without_out.size()); + REQUIRE_FALSE(with_out.empty()); for (size_t i = 0; i < with_out.size(); ++i) { - ensure_equals("with-VS15 glyph_id matches without-VS15", - with_out[i].glyph_id, without_out[i].glyph_id); + CAPTURE(i); + REQUIRE(with_out[i].glyph_id == without_out[i].glyph_id); } } // Identity check between codepoint→glyph (cmap) and shape result - // for a non-Latin codepoint that DejaVuSans covers. test<2> covers - // ASCII; this extends the contract to the upper BMP so a regression + // for a non-Latin codepoint that DejaVuSans covers. The ASCII case + // covers ASCII; this extends the contract to the upper BMP so a regression // limited to Unicode-cmap binding (Defect B in the audit plan) is // caught even when ASCII still works. - template<> template<> - void alfontshaping_object::test<31>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: a non-ASCII codepoint shapes to the glyph the cmap gives it", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); // Pick the first non-ASCII codepoint the face has. DejaVuSans // covers Greek and Cyrillic; try a Greek alpha (U+03B1) first. @@ -1337,16 +1287,14 @@ namespace } } if (wch == 0) - skip("no non-ASCII codepoint covered in DejaVuSans"); + SKIP("no non-ASCII codepoint covered in DejaVuSans"); const Text s = text(wch); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure_equals("1 glyph for non-ASCII codepoint", out.size(), 1u); - ensure_equals("shape glyph_id matches cmap lookup", - out[0].glyph_id, ft->getCharGlyphIndex(wch)); - ensure("non-ASCII glyph has positive advance", - out[0].x_advance > 0.f); + REQUIRE(out.size() == 1u); + CHECK(out[0].glyph_id == ft->getCharGlyphIndex(wch)); + CHECK(out[0].x_advance > 0.f); } // Cluster index invariant: every glyph emitted by shapeRun has @@ -1355,14 +1303,13 @@ namespace // that index per-cluster advances by cluster. ZWJ-retry candidates // and corrupt GSUB tables can synthesize cluster values outside the // input range; the clamp pins them back into the slice. - template<> template<> - void alfontshaping_object::test<32>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: every cluster shapeRun emits lies within the slice it shaped", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); // begin/end are written as codepoint indices and converted per case. struct Case { Text s; size_t begin; size_t end; }; @@ -1378,6 +1325,7 @@ namespace for (size_t ci = 0; ci < cases.size(); ++ci) { + CAPTURE(ci); const auto& c = cases[ci]; const size_t begin_bytes = c.s.at(c.begin); const size_t end_bytes = c.s.at(c.end); @@ -1385,10 +1333,8 @@ namespace ALFontShaping::shapeRun(ft, c.s, begin_bytes, end_bytes, out); for (const auto& g : out) { - ensure(("cluster >= begin (case " + std::to_string(ci) + ")").c_str(), - g.cluster >= (S32)begin_bytes); - ensure(("cluster < end (case " + std::to_string(ci) + ")").c_str(), - g.cluster < (S32)end_bytes); + REQUIRE(g.cluster >= (S32)begin_bytes); + REQUIRE(g.cluster < (S32)end_bytes); } } } @@ -1405,14 +1351,13 @@ namespace // fallback) — DejaVu has ZWJ but lacks the astral pictographs and // the GSUB rule for the composed flag, so HB emits one glyph per // covered codepoint plus notdefs. - template<> template<> - void alfontshaping_object::test<33>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: every glyph of an unligated emoji cluster carries the cluster's start", "[ALFontShaping]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); + REQUIRE(ft.notNull()); // "X" + trans flag (5 cps, [1, 6)) + "Y". The cluster walker // identifies [1, 6) as one emoji cluster; the fast path shapes it @@ -1423,7 +1368,7 @@ namespace 0xFE0F, L'Y'); std::vector out; ALFontShaping::shapeRun(ft, s, 0, s.size(), out); - ensure("shape produced output", !out.empty()); + REQUIRE_FALSE(out.empty()); // Every glyph whose cluster falls inside the flag must report the // cluster's start. Glyphs for X and Y are unaffected. @@ -1434,12 +1379,12 @@ namespace { if (g.cluster >= flag_begin && g.cluster < flag_end) saw_cluster_glyph = true; - ensure("trans-flag cluster glyphs collapse to cluster start", - !(g.cluster > flag_begin && g.cluster < flag_end)); + CAPTURE(g.cluster, flag_begin, flag_end); + REQUIRE_FALSE((g.cluster > flag_begin && g.cluster < flag_end)); } - ensure("at least one glyph routed through the cluster sub-run " - "(otherwise this test isn't exercising the fast path)", - saw_cluster_glyph); + UNSCOPED_INFO("at least one glyph routed through the cluster sub-run " + "(otherwise this test isn't exercising the fast path)"); + CHECK(saw_cluster_glyph); } // addFallbackFont must invalidate cached shape entries for the head: @@ -1452,46 +1397,43 @@ namespace // ALFontShaping cache dirty — so the second shape call returned the // stale entry and rendered tofu through A's notdef even though the // CJK fallback was attached and would have covered the codepoint. - template<> template<> - void alfontshaping_object::test<34>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: addFallbackFont drops cached shapes so the new fallback is used", "[ALFontShaping]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + SourceHanSans required"); + SKIP("DejaVuSans + SourceHanSans required"); LLPointer a = loadFt(a_path); LLPointer b = loadFt(b_path); - ensure("both loaded", a.notNull() && b.notNull()); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); // Pre-fallback shape: U+4F60 (你) is not in DejaVuSans's cmap, so // HB returns one notdef glyph (glyph_id=0) on face A. const Text s = text(0x4F60); std::vector pre; ALFontShaping::shapeRun(a, s, 0, s.size(), pre); - ensure_equals("pre-fallback shape produces 1 glyph", pre.size(), 1u); - ensure_equals("pre-fallback glyph routes to head A", pre[0].face, a.get()); - ensure_equals("pre-fallback glyph is notdef (glyph_id=0)", - pre[0].glyph_id, 0u); - ensure_equals("pre-fallback shape leaves one cache entry", - ALFontShaping::cacheSize(), 1u); + REQUIRE(pre.size() == 1u); + CHECK(pre[0].face == a.get()); + CHECK(pre[0].glyph_id == 0u); + UNSCOPED_INFO("pre-fallback shape leaves one cache entry"); + CHECK(ALFontShaping::cacheSize() == 1u); // Attach the CJK fallback. addFallbackFont must clear the cached // entry for `a` so the next shape() re-itemizes through the new // chain instead of returning the stale notdef. a->addFallbackFont(b); - ensure_equals("addFallbackFont(this) drops cached entries for this", - ALFontShaping::cacheSize(), 0u); + UNSCOPED_INFO("addFallbackFont(this) drops cached entries for this"); + CHECK(ALFontShaping::cacheSize() == 0u); // Post-fallback shape: U+4F60 should now route to face B with a // non-zero glyph_id (B has the CJK glyph in its cmap). std::vector post; ALFontShaping::shapeRun(a, s, 0, s.size(), post); - ensure_equals("post-fallback shape produces 1 glyph", post.size(), 1u); - ensure_equals("post-fallback glyph routes to fallback B", - post[0].face, b.get()); - ensure_not_equals("post-fallback glyph is real (not notdef)", - post[0].glyph_id, 0u); + REQUIRE(post.size() == 1u); + CHECK(post[0].face == b.get()); + CHECK(post[0].glyph_id != 0u); } // Two Unicode tables live in this process: HarfBuzz's own, which its @@ -1504,8 +1446,7 @@ namespace // They agree today, across the whole range. This holds them to it: bumping // either package past the other's Unicode version should fail here rather // than quietly change how text is shaped. - template<> template<> - void alfontshaping_object::test<35>() + TEST_CASE_METHOD(alfontshaping_data, "ALFontShaping: HarfBuzz's and ICU's Unicode tables agree on every codepoint", "[ALFontShaping]") { auto icu_to_hb = [](int8_t t) -> hb_unicode_general_category_t { @@ -1565,11 +1506,11 @@ namespace } } - ensure_equals("HarfBuzz and ICU agree on general category" - " (first disagreement at U+" + llformat("%04X", first_category) + ")", - category_diffs, 0); - ensure_equals("HarfBuzz and ICU agree on combining class" - " (first disagreement at U+" + llformat("%04X", first_combining) + ")", - combining_diffs, 0); + UNSCOPED_INFO("HarfBuzz and ICU agree on general category" + " (first disagreement at U+" + llformat("%04X", first_category) + ")"); + CHECK(category_diffs == 0); + UNSCOPED_INFO("HarfBuzz and ICU agree on combining class" + " (first disagreement at U+" + llformat("%04X", first_combining) + ")"); + CHECK(combining_diffs == 0); } } diff --git a/indra/llrender/tests/algpubuffer_gl_test.cpp b/indra/llrender/tests/algpubuffer_gl_test.cpp index ec140236a6..647cfda0c5 100644 --- a/indra/llrender/tests/algpubuffer_gl_test.cpp +++ b/indra/llrender/tests/algpubuffer_gl_test.cpp @@ -35,17 +35,15 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include #include -namespace tut +namespace { - namespace - { constexpr U32 WORDS = 64; // A point at the centre, from no vertex arrays. @@ -104,11 +102,15 @@ namespace tut } // A program that draws a point and declares no inputs, bound, for the indirect draws. - void setUpProgram() + void require_program() { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram.mProgramObject = glCreateProgram(); mProgram.mName = "indirect point (test)"; mProgram.attachObject(vs); @@ -116,8 +118,8 @@ namespace tut const bool linked = mProgram.mapAttributes(); glDeleteShader(vs); glDeleteShader(fs); - ensure("program links", linked); - ensure("program maps", mProgram.mapUniforms()); + REQUIRE(linked); + REQUIRE(mProgram.mapUniforms()); mProgram.bind(); } @@ -126,231 +128,255 @@ namespace tut LLGLSLShader mProgram; }; - typedef test_group algpubuffer_t; - typedef algpubuffer_t::object algpubuffer_object_t; - tut::algpubuffer_t tut_algpubuffer("algpubuffer_gl"); - } // What a buffer allocated with data holds, what an upload, a clear of a range and a whole clear leave, and what // the store accounts for while it lives. - template<> template<> - void algpubuffer_object_t::test<1>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: a DYNAMIC buffer holds what is written and accounts its store", "[algpubuffer_gl]") { const U64 before = ALGPUBuffer::sBytesAllocated; std::vector expected = pattern(WORDS, 1000u); { ALGPUBuffer buffer; - ensure("allocates", buffer.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, expected.data(), "words (test)")); - ensure("allocated", buffer.isAllocated()); - ensure_equals("its size", buffer.size(), size_t(WORDS * sizeof(U32))); - ensure_equals("its flags", buffer.getFlags(), U32(ALGPUBuffer::DYNAMIC)); - ensure_equals("accounted", ALGPUBuffer::sBytesAllocated, before + WORDS * sizeof(U32)); - ensure("holds its data", readAll(buffer) == expected); + REQUIRE(buffer.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, expected.data(), "words (test)")); + CHECK(buffer.isAllocated()); + REQUIRE(buffer.size() == size_t(WORDS * sizeof(U32))); + CHECK(buffer.getFlags() == U32(ALGPUBuffer::DYNAMIC)); + CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); + UNSCOPED_INFO("holds its data"); + CHECK(readAll(buffer) == expected); const std::vector middle = pattern(8, 77u); buffer.upload(16 * sizeof(U32), middle.data(), middle.size() * sizeof(U32)); std::copy(middle.begin(), middle.end(), expected.begin() + 16); - ensure("holds the upload where it went", readAll(buffer) == expected); + UNSCOPED_INFO("holds the upload where it went"); + CHECK(readAll(buffer) == expected); buffer.clearRange(40 * sizeof(U32), 6 * sizeof(U32), 0xDEADBEEFu); std::fill(expected.begin() + 40, expected.begin() + 46, 0xDEADBEEFu); - ensure("holds the cleared range, and only it", readAll(buffer) == expected); + UNSCOPED_INFO("holds the cleared range, and only it"); + CHECK(readAll(buffer) == expected); std::vector part(4, 0u); - ensure("reads at an offset", buffer.read(42 * sizeof(U32), part.data(), part.size() * sizeof(U32))); - ensure("the words there", part == std::vector(expected.begin() + 42, expected.begin() + 46)); + REQUIRE(buffer.read(42 * sizeof(U32), part.data(), part.size() * sizeof(U32))); + CHECK(part == std::vector(expected.begin() + 42, expected.begin() + 46)); buffer.clear(7u); - ensure("whole clear", readAll(buffer) == std::vector(WORDS, 7u)); + CHECK(readAll(buffer) == std::vector(WORDS, 7u)); } - ensure_equals("destruction returns what it accounted", ALGPUBuffer::sBytesAllocated, before); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("destruction returns what it accounted"); + CHECK(ALGPUBuffer::sBytesAllocated == before); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Copies between buffers that are not DYNAMIC, and within one, go where they are told; and such a buffer clears // where the clear is the GPU's. Below GL 4.3 the clear is an upload, which a static store does not take, and is // asserted: the buffers a clear is for are compute's, and compute needs 4.3. - template<> template<> - void algpubuffer_object_t::test<2>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: copies between and within static buffers land where they are told", "[algpubuffer_gl]") { const std::vector source_words = pattern(WORDS, 5u); std::vector expected(WORDS, 0u); ALGPUBuffer source; ALGPUBuffer target; - ensure("source allocates", source.allocate(WORDS * sizeof(U32), 0, source_words.data(), "source (test)")); - ensure("target allocates", target.allocate(WORDS * sizeof(U32), 0, expected.data(), "target (test)")); + REQUIRE(source.allocate(WORDS * sizeof(U32), 0, source_words.data(), "source (test)")); + REQUIRE(target.allocate(WORDS * sizeof(U32), 0, expected.data(), "target (test)")); target.copyFrom(source, 4 * sizeof(U32), 32 * sizeof(U32), 10 * sizeof(U32)); std::copy(source_words.begin() + 4, source_words.begin() + 14, expected.begin() + 32); - ensure("copied between buffers", readAll(target) == expected); + UNSCOPED_INFO("copied between buffers"); + CHECK(readAll(target) == expected); target.copyFrom(target, 32 * sizeof(U32), 0, 10 * sizeof(U32)); std::copy(expected.begin() + 32, expected.begin() + 42, expected.begin()); - ensure("copied within one", readAll(target) == expected); - ensure("the source untouched", readAll(source) == source_words); + UNSCOPED_INFO("copied within one"); + CHECK(readAll(target) == expected); + CHECK(readAll(source) == source_words); if (clearsOnGPU()) { target.clear(0xA5A5A5A5u); - ensure("a static buffer clears", readAll(target) == std::vector(WORDS, 0xA5A5A5A5u)); + CHECK(readAll(target) == std::vector(WORDS, 0xA5A5A5A5u)); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // release empties a buffer and returns its accounting; a move takes the store and leaves the source empty, and a // move over a buffer that held a store releases that store. - template<> template<> - void algpubuffer_object_t::test<3>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: release empties a buffer and a move takes its store", "[algpubuffer_gl]") { const U64 before = ALGPUBuffer::sBytesAllocated; const std::vector words = pattern(WORDS, 9u); ALGPUBuffer first; - ensure("allocates", first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); + REQUIRE(first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); first.release(); - ensure("released", !first.isAllocated()); - ensure_equals("no size", first.size(), size_t(0)); - ensure_equals("no name", first.getGLName(), 0u); - ensure_equals("accounting returned", ALGPUBuffer::sBytesAllocated, before); + CHECK_FALSE(first.isAllocated()); + CHECK(first.size() == size_t(0)); + CHECK(first.getGLName() == 0u); + UNSCOPED_INFO("accounting returned"); + CHECK(ALGPUBuffer::sBytesAllocated == before); - ensure("allocates again", first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); + REQUIRE(first.allocate(WORDS * sizeof(U32), 0, words.data(), "first (test)")); const U32 name = first.getGLName(); ALGPUBuffer moved(std::move(first)); - ensure("the source is empty", !first.isAllocated() && first.size() == 0 && first.getGLName() == 0); - ensure_equals("the target took the name", moved.getGLName(), name); - ensure("and the store", readAll(moved) == words); - ensure_equals("accounted once", ALGPUBuffer::sBytesAllocated, before + WORDS * sizeof(U32)); + { + INFO("the source is empty"); + CHECK_FALSE(first.isAllocated()); + CHECK(first.size() == 0); + CHECK(first.getGLName() == 0); + } + CHECK(moved.getGLName() == name); + CHECK(readAll(moved) == words); + UNSCOPED_INFO("accounted once"); + CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); ALGPUBuffer other; - ensure("other allocates", other.allocate(2 * WORDS * sizeof(U32), 0, nullptr, "other (test)")); + REQUIRE(other.allocate(2 * WORDS * sizeof(U32), 0, nullptr, "other (test)")); other = std::move(moved); - ensure("the moved-from is empty", !moved.isAllocated() && moved.getGLName() == 0); - ensure_equals("the assigned took the name", other.getGLName(), name); - ensure("and the store", readAll(other) == words); - ensure_equals("its own store released", ALGPUBuffer::sBytesAllocated, before + WORDS * sizeof(U32)); + { + INFO("the moved-from is empty"); + CHECK_FALSE(moved.isAllocated()); + CHECK(moved.getGLName() == 0); + } + CHECK(other.getGLName() == name); + CHECK(readAll(other) == words); + UNSCOPED_INFO("its own store released"); + CHECK(ALGPUBuffer::sBytesAllocated == before + WORDS * sizeof(U32)); other.release(); - ensure_equals("all returned", ALGPUBuffer::sBytesAllocated, before); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("all returned"); + CHECK(ALGPUBuffer::sBytesAllocated == before); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // LLRender binds an indirect draw's buffer once and keeps it bound; after refreshState it binds it again, since // what is bound is no longer known; and a buffer's release forgets its name, so a new buffer that GL hands the // same name is bound rather than taken for bound already. - template<> template<> - void algpubuffer_object_t::test<4>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: LLRender keeps an indirect draw's buffer bound and rebinds when it must", "[algpubuffer_gl]") { - setUpProgram(); + require_program(); ALDrawArraysIndirectCommand command; command.mCount = 1; command.mInstanceCount = 1; ALGPUBuffer first; - ensure("allocates", first.allocate(sizeof(command), 0, &command, "draw (test)")); + REQUIRE(first.allocate(sizeof(command), 0, &command, "draw (test)")); gGL.drawArraysIndirect(LLRender::POINTS, first, 0); - ensure_equals("bound for the draw", indirectBinding(), (GLint)first.getGLName()); + UNSCOPED_INFO("bound for the draw"); + CHECK(indirectBinding() == (GLint)first.getGLName()); gGL.drawArraysIndirect(LLRender::POINTS, first, 0); - ensure_equals("still bound", indirectBinding(), (GLint)first.getGLName()); + UNSCOPED_INFO("still bound"); + CHECK(indirectBinding() == (GLint)first.getGLName()); // Behind LLRender's back. glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0); gGL.refreshState(); gGL.drawArraysIndirect(LLRender::POINTS, first, 0); - ensure_equals("bound again after refreshState", indirectBinding(), (GLint)first.getGLName()); - ensure_equals("no GL error after refreshState", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("bound again after refreshState"); + CHECK(indirectBinding() == (GLint)first.getGLName()); + UNSCOPED_INFO("after refreshState"); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); const U32 name = first.getGLName(); first.release(); - ensure_equals("deleting it unbound it", indirectBinding(), 0); + UNSCOPED_INFO("deleting it unbound it"); + CHECK(indirectBinding() == 0); ALGPUBuffer second; - ensure("another allocates", second.allocate(sizeof(command), 0, &command, "draw again (test)")); + REQUIRE(second.allocate(sizeof(command), 0, &command, "draw again (test)")); if (second.getGLName() != name) { - skip("GL did not hand the released name out again"); + SKIP("GL did not hand the released name out again"); } gGL.drawArraysIndirect(LLRender::POINTS, second, 0); - ensure_equals("the reused name is bound", indirectBinding(), (GLint)second.getGLName()); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the reused name is bound"); + CHECK(indirectBinding() == (GLint)second.getGLName()); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A READBACK store is mapped where persistent maps resolve, and a STREAM one too; an upload into a mapped STREAM // store goes through the map, and the GPU sees it in a copy issued after it. - template<> template<> - void algpubuffer_object_t::test<5>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: READBACK and STREAM stores are mapped where persistent maps resolve", "[algpubuffer_gl]") { const bool persistent = glBufferStorage != nullptr; ALGPUBuffer readback; ALGPUBuffer stream; ALGPUBuffer check; - ensure("readback allocates", readback.allocate(WORDS * sizeof(U32), ALGPUBuffer::READBACK, nullptr, "readback (test)")); - ensure("stream allocates", stream.allocate(WORDS * sizeof(U32), ALGPUBuffer::STREAM, nullptr, "stream (test)")); - ensure("check allocates", check.allocate(WORDS * sizeof(U32), 0, nullptr, "check (test)")); - ensure_equals("readback mapped where persistent maps resolve", readback.getMapped() != nullptr, persistent); - ensure_equals("stream too", stream.getMapped() != nullptr, persistent); + REQUIRE(readback.allocate(WORDS * sizeof(U32), ALGPUBuffer::READBACK, nullptr, "readback (test)")); + REQUIRE(stream.allocate(WORDS * sizeof(U32), ALGPUBuffer::STREAM, nullptr, "stream (test)")); + REQUIRE(check.allocate(WORDS * sizeof(U32), 0, nullptr, "check (test)")); + UNSCOPED_INFO("readback mapped where persistent maps resolve"); + CHECK((readback.getMapped() != nullptr) == persistent); + UNSCOPED_INFO("stream mapped where persistent maps resolve"); + CHECK((stream.getMapped() != nullptr) == persistent); const std::vector words = pattern(WORDS, 300u); stream.upload(0, words.data(), words.size() * sizeof(U32)); check.copyFrom(stream, 0, 0, WORDS * sizeof(U32)); - ensure("the GPU sees the stream's upload", readAll(check) == words); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the GPU sees the stream's upload"); + CHECK(readAll(check) == words); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // ALGPUReadback: each request copies what its source held then; a request with every slot pending is refused; the // results come back oldest first; and with none pending collect has nothing to give. - template<> template<> - void algpubuffer_object_t::test<6>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: ALGPUReadback gives back each request's copy oldest first", "[algpubuffer_gl]") { constexpr U32 SLOTS = 3; constexpr U32 COUNT = 16; ALGPUBuffer source; - ensure("source allocates", source.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, nullptr, "source (test)")); + REQUIRE(source.allocate(WORDS * sizeof(U32), ALGPUBuffer::DYNAMIC, nullptr, "source (test)")); ALGPUReadback readback; - ensure("readback allocates", readback.allocate(COUNT * sizeof(U32), SLOTS, "readback (test)")); - ensure_equals("a slot's size", readback.getSlotSize(), size_t(COUNT * sizeof(U32))); + REQUIRE(readback.allocate(COUNT * sizeof(U32), SLOTS, "readback (test)")); + CHECK(readback.getSlotSize() == size_t(COUNT * sizeof(U32))); std::vector result(COUNT, 0u); - ensure("nothing to collect before a request", !readback.collect(result.data(), result.size() * sizeof(U32))); + UNSCOPED_INFO("nothing to collect before a request"); + REQUIRE_FALSE(readback.collect(result.data(), result.size() * sizeof(U32))); for (U32 i = 0; i < SLOTS; ++i) { const std::vector words = pattern(WORDS, 1000u * (i + 1)); source.upload(0, words.data(), words.size() * sizeof(U32)); - ensure("request " + std::to_string(i), readback.request(source, 8 * sizeof(U32), COUNT * sizeof(U32))); + CAPTURE(i); + REQUIRE(readback.request(source, 8 * sizeof(U32), COUNT * sizeof(U32))); } - ensure_equals("every slot pending", readback.getPending(), SLOTS); - ensure("a request past them refused", !readback.request(source, 0, COUNT * sizeof(U32))); + CHECK(readback.getPending() == SLOTS); + UNSCOPED_INFO("a request past them refused"); + CHECK_FALSE(readback.request(source, 0, COUNT * sizeof(U32))); for (U32 i = 0; i < SLOTS; ++i) { - ensure("collect " + std::to_string(i), readback.collect(result.data(), result.size() * sizeof(U32), true)); + CAPTURE(i); + REQUIRE(readback.collect(result.data(), result.size() * sizeof(U32), true)); const std::vector words = pattern(WORDS, 1000u * (i + 1)); - ensure("result " + std::to_string(i) + " is what the source held at its request", - result == std::vector(words.begin() + 8, words.begin() + 8 + COUNT)); + UNSCOPED_INFO("what the source held at its request"); + REQUIRE(result == std::vector(words.begin() + 8, words.begin() + 8 + COUNT)); } - ensure_equals("none pending", readback.getPending(), 0u); - ensure("nothing more to collect", !readback.collect(result.data(), result.size() * sizeof(U32), true)); + CHECK(readback.getPending() == 0u); + UNSCOPED_INFO("nothing more to collect"); + REQUIRE_FALSE(readback.collect(result.data(), result.size() * sizeof(U32), true)); // A request made, its copy finished: collect gives it without waiting. const std::vector last = pattern(WORDS, 7u); source.upload(0, last.data(), last.size() * sizeof(U32)); - ensure("one more request", readback.request(source, 0, COUNT * sizeof(U32))); + CHECK(readback.request(source, 0, COUNT * sizeof(U32))); glFinish(); - ensure("a finished copy is collected without waiting", readback.collect(result.data(), result.size() * sizeof(U32))); - ensure("and holds the source", result == std::vector(last.begin(), last.begin() + COUNT)); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("a finished copy is collected without waiting"); + REQUIRE(readback.collect(result.data(), result.size() * sizeof(U32))); + UNSCOPED_INFO("and holds the source"); + CHECK(result == std::vector(last.begin(), last.begin() + COUNT)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // ALGPUStream: each map gives the next region, round the ring, at offsets a storage range may start at; what is // written there and published is what the GPU reads at the offset publish returns; and the ring wraps. - template<> template<> - void algpubuffer_object_t::test<7>() + TEST_CASE_METHOD(algpubuffer_data, "algpubuffer_gl: ALGPUStream maps each region in turn round the ring", "[algpubuffer_gl]") { constexpr U32 REGIONS = 3; constexpr U32 COUNT = 10; ALGPUStream stream; - ensure("stream allocates", stream.allocate(COUNT * sizeof(U32), REGIONS, "stream (test)")); - ensure_equals("a region's size", stream.getRegionSize(), size_t(COUNT * sizeof(U32))); + REQUIRE(stream.allocate(COUNT * sizeof(U32), REGIONS, "stream (test)")); + CHECK(stream.getRegionSize() == size_t(COUNT * sizeof(U32))); ALGPUBuffer check; - ensure("check allocates", check.allocate(COUNT * sizeof(U32), 0, nullptr, "check (test)")); + REQUIRE(check.allocate(COUNT * sizeof(U32), 0, nullptr, "check (test)")); const size_t alignment = gGLManager.mShaderStorageBufferOffsetAlignment > 0 ? (size_t)gGLManager.mShaderStorageBufferOffsetAlignment @@ -358,25 +384,28 @@ namespace tut std::vector offsets; for (U32 frame = 0; frame < 2 * REGIONS; ++frame) { + CAPTURE(frame); U8* region = stream.map(); - ensure("a region to write", region != nullptr); + REQUIRE(region != nullptr); const std::vector words = pattern(COUNT, 100u * (frame + 1)); memcpy(region, words.data(), words.size() * sizeof(U32)); const size_t offset = stream.publish(words.size() * sizeof(U32)); - ensure("frame " + std::to_string(frame) + " at an aligned offset", offset % alignment == 0); + REQUIRE(offset % alignment == 0); offsets.push_back(offset); check.copyFrom(stream.getBuffer(), offset, 0, COUNT * sizeof(U32)); - ensure("frame " + std::to_string(frame) + " reaches the GPU", readAll(check) == words); + REQUIRE(readAll(check) == words); } for (U32 frame = 0; frame < REGIONS; ++frame) { for (U32 other = frame + 1; other < REGIONS; ++other) { - ensure("the ring's regions apart", offsets[frame] != offsets[other]); + UNSCOPED_INFO("the ring's regions apart"); + REQUIRE(offsets[frame] != offsets[other]); } - ensure_equals("the ring wraps to the same region", offsets[frame + REGIONS], offsets[frame]); + UNSCOPED_INFO("the ring wraps to the same region"); + REQUIRE(offsets[frame + REGIONS] == offsets[frame]); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alimageslot_gl_test.cpp b/indra/llrender/tests/alimageslot_gl_test.cpp index f51ef9b00b..3d5c1545eb 100644 --- a/indra/llrender/tests/alimageslot_gl_test.cpp +++ b/indra/llrender/tests/alimageslot_gl_test.cpp @@ -38,16 +38,14 @@ #include "llimage.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { - namespace - { constexpr U32 SIZE = 8; // What GL holds at image unit `unit`. @@ -121,16 +119,15 @@ namespace tut // Compute in use and two R32F targets with a mip level beyond the first, or the case skipped where the // context has no images. - void setUp() + void require_targets() { if (!ll_test::contextHasCompute()) { - skip("the context has no compute"); + SKIP("the context has no compute"); } LLRender::sCompute = true; - ensure("target allocates", - mTarget.allocate(SIZE, SIZE, GL_R32F, false, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_MANUAL)); - ensure("other target allocates", mOther.allocate(SIZE, SIZE, GL_RGBA16F)); + REQUIRE(mTarget.allocate(SIZE, SIZE, GL_R32F, false, false, ALTextureSlot::TT_TEXTURE, LLRenderTarget::MIPS_MANUAL)); + REQUIRE(mOther.allocate(SIZE, SIZE, GL_RGBA16F)); } ll_test::SharedGLScope mGL; @@ -140,106 +137,118 @@ namespace tut GLuint mKernel = 0; }; - typedef test_group alimageslot_t; - typedef alimageslot_t::object alimageslot_object_t; - tut::alimageslot_t tut_alimageslot("alimageslot_gl"); - } // A bind reaches GL as stated, a bind of what the unit already holds issues nothing, a change of any part of it // issues one call, and unbind returns the unit to texture 0 once. - template<> template<> - void alimageslot_object_t::test<1>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: a bind reaches GL as stated and only a change issues a call", "[alimageslot_gl]") { - setUp(); + require_targets(); ALImageSlot* slot = gGL.getImageSlot(2); - ensure_equals("the slot is its unit", slot->getIndex(), 2); + CHECK(slot->getIndex() == 2); U32 binds = ALImageSlot::sImageBinds; - ensure("bound", slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); - ensure_equals("one call", ALImageSlot::sImageBinds, binds + 1); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); + UNSCOPED_INFO("the bind issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 1); UnitState state = unitState(2); - ensure_equals("GL holds the texture", state.mTexture, (GLint)mTarget.getTexture()); - ensure_equals("at its level", state.mLevel, 1); - ensure_equals("not layered", state.mLayered, GL_FALSE); - ensure_equals("read-write", state.mAccess, GL_READ_WRITE); - ensure_equals("at the target's own format", state.mFormat, GL_R32F); - ensure_equals("the slot says so", slot->getCurrTexture(), mTarget.getTexture()); + CHECK(state.mTexture == (GLint)mTarget.getTexture()); + CHECK(state.mLevel == 1); + CHECK(state.mLayered == GL_FALSE); + CHECK(state.mAccess == GL_READ_WRITE); + UNSCOPED_INFO("at the target's own format"); + CHECK(state.mFormat == GL_R32F); + CHECK(slot->getCurrTexture() == mTarget.getTexture()); binds = ALImageSlot::sImageBinds; - ensure("bound again", slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); - ensure_equals("the same bind issues nothing", ALImageSlot::sImageBinds, binds); - - ensure("another access", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 1)); - ensure_equals("a new access issues one call", ALImageSlot::sImageBinds, binds + 1); - ensure_equals("GL holds the new access", unitState(2).mAccess, GL_READ_ONLY); - - ensure("another level", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 0)); - ensure_equals("a new level issues one call", ALImageSlot::sImageBinds, binds + 2); - ensure_equals("GL holds the new level", unitState(2).mLevel, 0); - - ensure("another texture", slot->bind(&mOther, 0, ALImageSlot::READ_ONLY, 0)); - ensure_equals("a new texture issues one call", ALImageSlot::sImageBinds, binds + 3); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_WRITE, 1)); + UNSCOPED_INFO("the same bind issues nothing"); + CHECK(ALImageSlot::sImageBinds == binds); + + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 1)); + UNSCOPED_INFO("a new access issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 1); + CHECK(unitState(2).mAccess == GL_READ_ONLY); + + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY, 0)); + UNSCOPED_INFO("a new level issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 2); + CHECK(unitState(2).mLevel == 0); + + CHECK(slot->bind(&mOther, 0, ALImageSlot::READ_ONLY, 0)); + UNSCOPED_INFO("a new texture issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 3); state = unitState(2); - ensure_equals("GL holds the other texture", state.mTexture, (GLint)mOther.getTexture()); - ensure_equals("at its own format", state.mFormat, GL_RGBA16F); + CHECK(state.mTexture == (GLint)mOther.getTexture()); + UNSCOPED_INFO("at its own format"); + CHECK(state.mFormat == GL_RGBA16F); slot->unbind(); - ensure_equals("unbind issues one call", ALImageSlot::sImageBinds, binds + 4); - ensure_equals("GL holds no texture", unitState(2).mTexture, 0); - ensure_equals("nor does the slot", slot->getCurrTexture(), 0u); + UNSCOPED_INFO("unbind issues one call"); + CHECK(ALImageSlot::sImageBinds == binds + 4); + CHECK(unitState(2).mTexture == 0); + CHECK(slot->getCurrTexture() == 0u); slot->unbind(); - ensure_equals("unbinding nothing issues nothing", ALImageSlot::sImageBinds, binds + 4); + UNSCOPED_INFO("unbinding nothing issues nothing"); + CHECK(ALImageSlot::sImageBinds == binds + 4); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A unit changed behind LLRender's back is put back by refreshState, every part of it. - template<> template<> - void alimageslot_object_t::test<2>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: refreshState puts back every part of a unit changed behind its back", "[alimageslot_gl]") { - setUp(); - ensure("bound", gGL.getImageSlot(3)->bind(&mTarget, 0, ALImageSlot::WRITE_ONLY, 1)); - ensure("other bound", gGL.getImageSlot(4)->bind(&mOther, 0, ALImageSlot::READ_ONLY)); + require_targets(); + CHECK(gGL.getImageSlot(3)->bind(&mTarget, 0, ALImageSlot::WRITE_ONLY, 1)); + CHECK(gGL.getImageSlot(4)->bind(&mOther, 0, ALImageSlot::READ_ONLY)); glBindImageTexture(3, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8); glBindImageTexture(4, mTarget.getTexture(), 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32F); - ensure_equals("clobbered", unitState(3).mTexture, 0); + UNSCOPED_INFO("clobbered"); + CHECK(unitState(3).mTexture == 0); gGL.refreshState(); UnitState state = unitState(3); - ensure_equals("the texture put back", state.mTexture, (GLint)mTarget.getTexture()); - ensure_equals("its level put back", state.mLevel, 1); - ensure_equals("its access put back", state.mAccess, GL_WRITE_ONLY); - ensure_equals("its format put back", state.mFormat, GL_R32F); + { + INFO("unit 3 put back"); + CHECK(state.mTexture == (GLint)mTarget.getTexture()); + CHECK(state.mLevel == 1); + CHECK(state.mAccess == GL_WRITE_ONLY); + CHECK(state.mFormat == GL_R32F); + } state = unitState(4); - ensure_equals("the other texture put back", state.mTexture, (GLint)mOther.getTexture()); - ensure_equals("its access put back", state.mAccess, GL_READ_ONLY); - ensure_equals("its format put back", state.mFormat, GL_RGBA16F); + { + INFO("unit 4 put back"); + CHECK(state.mTexture == (GLint)mOther.getTexture()); + CHECK(state.mAccess == GL_READ_ONLY); + CHECK(state.mFormat == GL_RGBA16F); + } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // forgetTextures drops a unit holding a name about to be deleted, and only that one, so the name bound again // afterwards -- which GL is free to hand to a new texture -- is bound rather than skipped. And deleting a target // the viewer's way, through LLImageGL's deferred delete, reaches it. - template<> template<> - void alimageslot_object_t::test<3>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: forgetTextures drops only the units that held a deleted name", "[alimageslot_gl]") { - setUp(); + require_targets(); ALImageSlot* slot = gGL.getImageSlot(1); ALImageSlot* kept = gGL.getImageSlot(5); - ensure("bound", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); - ensure("other bound", kept->bind(&mOther, 0, ALImageSlot::READ_ONLY)); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + CHECK(kept->bind(&mOther, 0, ALImageSlot::READ_ONLY)); const U32 name = mTarget.getTexture(); gGL.forgetTextures(1, &name); - ensure_equals("the slot forgot the name", slot->getCurrTexture(), 0u); - ensure_equals("the other slot did not", kept->getCurrTexture(), mOther.getTexture()); + UNSCOPED_INFO("the slot forgot the name"); + CHECK(slot->getCurrTexture() == 0u); + UNSCOPED_INFO("the other slot did not"); + CHECK(kept->getCurrTexture() == mOther.getTexture()); const U32 binds = ALImageSlot::sImageBinds; - ensure("bound again", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); - ensure_equals("the forgotten name is bound, not skipped", ALImageSlot::sImageBinds, binds + 1); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + UNSCOPED_INFO("the forgotten name is bound, not skipped"); + CHECK(ALImageSlot::sImageBinds == binds + 1); // The viewer's way: the target's textures go on LLImageGL's free list and are deleted some frames on. const U32 other = mOther.getTexture(); @@ -248,85 +257,93 @@ namespace tut { LLImageGL::updateClass(); } - ensure_equals("the deleted texture's slot forgot it", kept->getCurrTexture(), 0u); - ensure_equals("GL dropped it too", unitState(5).mTexture, 0); - ensure_equals("the live one stays", slot->getCurrTexture(), name); - ensure("the deleted name is gone", glIsTexture(other) == GL_FALSE); - - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the deleted texture's slot forgot it"); + CHECK(kept->getCurrTexture() == 0u); + UNSCOPED_INFO("GL dropped it too"); + CHECK(unitState(5).mTexture == 0); + UNSCOPED_INFO("the live one stays"); + CHECK(slot->getCurrTexture() == name); + CHECK(glIsTexture(other) == GL_FALSE); + + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // What an image cannot be bound to: an sRGB texture -- which an LLImageGL made from an image is -- a depth // texture, and a unit past the slots. Each is refused, with nothing issued and the unit unchanged; the same image // with a linear format, and a volume, bind. - template<> template<> - void alimageslot_object_t::test<4>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: sRGB and depth textures and units past the slots are refused", "[alimageslot_gl]") { - setUp(); + require_targets(); ALImageSlot* slot = gGL.getImageSlot(6); - ensure("bound", slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + CHECK(slot->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); const U32 binds = ALImageSlot::sImageBinds; LLPointer raw = new LLImageRaw(4, 4, 4); std::memset(raw->getData(), 0x80, 4 * 4 * 4); LLPointer srgb = new LLImageGL(raw, false); - ensure_equals("an image's texture is sRGB", srgb->getStorageInternalFormat(), (S32)GL_SRGB8_ALPHA8); - ensure("sRGB refused", !slot->bind(srgb.get(), ALImageSlot::READ_ONLY)); + CHECK(srgb->getStorageInternalFormat() == (S32)GL_SRGB8_ALPHA8); + CHECK_FALSE(slot->bind(srgb.get(), ALImageSlot::READ_ONLY)); LLRenderTarget depth; - ensure("depth target allocates", depth.allocate(SIZE, SIZE, 0, true)); - ensure("depth refused", !slot->bindManual(depth.getDepth(), 0, false, 0, ALImageSlot::READ_ONLY, + REQUIRE(depth.allocate(SIZE, SIZE, 0, true)); + CHECK_FALSE(slot->bindManual(depth.getDepth(), 0, false, 0, ALImageSlot::READ_ONLY, GL_DEPTH_COMPONENT24)); depth.release(); - ensure("past the slots refused", !gGL.getImageSlot(AL_NUM_IMAGE_SLOTS)->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); + CHECK_FALSE(gGL.getImageSlot(AL_NUM_IMAGE_SLOTS)->bind(&mTarget, 0, ALImageSlot::READ_ONLY)); - ensure_equals("nothing issued", ALImageSlot::sImageBinds, binds); - ensure_equals("the unit unchanged", unitState(6).mTexture, (GLint)mTarget.getTexture()); - ensure_equals("and the slot", slot->getCurrTexture(), mTarget.getTexture()); + UNSCOPED_INFO("nothing issued"); + CHECK(ALImageSlot::sImageBinds == binds); + UNSCOPED_INFO("the unit unchanged"); + CHECK(unitState(6).mTexture == (GLint)mTarget.getTexture()); + UNSCOPED_INFO("nor the slot"); + CHECK(slot->getCurrTexture() == mTarget.getTexture()); LLPointer linear = new LLImageGL(4, 4, 4, false); linear->setExplicitFormat(GL_RGBA8, GL_RGBA); - ensure("linear image created", linear->createGLTexture(0, raw.get())); - ensure("linear bound", slot->bind(linear.get(), ALImageSlot::READ_ONLY)); + REQUIRE(linear->createGLTexture(0, raw.get())); + CHECK(slot->bind(linear.get(), ALImageSlot::READ_ONLY)); UnitState state = unitState(6); - ensure_equals("GL holds the image", state.mTexture, (GLint)linear->getTexName()); - ensure_equals("at RGBA8", state.mFormat, GL_RGBA8); - ensure_equals("one layer", state.mLayered, GL_FALSE); + CHECK(state.mTexture == (GLint)linear->getTexName()); + CHECK(state.mFormat == GL_RGBA8); + CHECK(state.mLayered == GL_FALSE); LLPointer volume = new ALTexture3D(); - ensure("volume allocates", volume->allocate(4, 4, 4, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, nullptr)); - ensure("volume bound", slot->bind(volume.get(), ALImageSlot::WRITE_ONLY)); + REQUIRE(volume->allocate(4, 4, 4, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, nullptr)); + CHECK(slot->bind(volume.get(), ALImageSlot::WRITE_ONLY)); state = unitState(6); - ensure_equals("GL holds the volume", state.mTexture, (GLint)volume->getGLName()); - ensure_equals("every slice", state.mLayered, GL_TRUE); - ensure_equals("at its own format", state.mFormat, GL_RGBA16F); + CHECK(state.mTexture == (GLint)volume->getGLName()); + UNSCOPED_INFO("every slice"); + CHECK(state.mLayered == GL_TRUE); + UNSCOPED_INFO("at its own format"); + CHECK(state.mFormat == GL_RGBA16F); slot->unbind(); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An R32UI target and an R32UI image, each the r32ui image of a kernel that loads every texel, changes it and // stores it back. The target's texels start where LLRenderTarget::clear put them and the image's where its upload // did, and both come back changed exactly, past what a float could carry. - template<> template<> - void alimageslot_object_t::test<5>() + TEST_CASE_METHOD(alimageslot_data, "alimageslot_gl: a kernel changes R32UI target and image texels exactly", "[alimageslot_gl]") { - setUp(); + require_targets(); constexpr U32 W = 13; constexpr U32 H = 7; const GLuint cs = ll_test::compileTestShader(GL_COMPUTE_SHADER, kUintKernel); - ensure("kernel compiles", cs != 0); + UNSCOPED_INFO("kernel compiles"); + REQUIRE(cs != 0); mKernel = glCreateProgram(); glAttachShader(mKernel, cs); glLinkProgram(mKernel); glDeleteShader(cs); GLint linked = GL_FALSE; glGetProgramiv(mKernel, GL_LINK_STATUS, &linked); - ensure("kernel links", linked == GL_TRUE); + UNSCOPED_INFO("kernel links"); + REQUIRE(linked == GL_TRUE); - ensure("R32UI target allocates", mUint.allocate(W, H, GL_R32UI)); + REQUIRE(mUint.allocate(W, H, GL_R32UI)); mUint.bindTarget(); gGL.setClearColor(LLColor4(5.f, 0.f, 0.f, 0.f)); mUint.clear(); @@ -334,8 +351,8 @@ namespace tut ALImageSlot* slot = gGL.getImageSlot(0); glUseProgram(mKernel); - ensure("target bound", slot->bind(&mUint, 0, ALImageSlot::READ_WRITE)); - ensure_equals("as R32UI", unitState(0).mFormat, GL_R32UI); + CHECK(slot->bind(&mUint, 0, ALImageSlot::READ_WRITE)); + CHECK(unitState(0).mFormat == GL_R32UI); glDispatchCompute((W + 7) / 8, (H + 7) / 8, 1); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); const std::vector target = readUints(mUint.getTexture(), W, H); @@ -347,9 +364,9 @@ namespace tut } LLPointer image = new LLImageGL(W, H, 1, /*usemipmaps=*/false); image->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - ensure("R32UI image created", image->createGLTexture(0, reinterpret_cast(values.data()))); - ensure("image bound", slot->bind(image.get(), ALImageSlot::READ_WRITE)); - ensure_equals("as R32UI too", unitState(0).mFormat, GL_R32UI); + REQUIRE(image->createGLTexture(0, reinterpret_cast(values.data()))); + CHECK(slot->bind(image.get(), ALImageSlot::READ_WRITE)); + CHECK(unitState(0).mFormat == GL_R32UI); glDispatchCompute((W + 7) / 8, (H + 7) / 8, 1); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); const std::vector held = readUints(image->getTexName(), W, H); @@ -360,12 +377,13 @@ namespace tut { for (U32 x = 0; x < W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = (size_t)y * W + x; - ensure_equals("target" + at, target[i], 5u * 3u + x + y * 100u); - ensure_equals("image" + at, held[i], values[i] * 3u + x + y * 100u); + REQUIRE(target[i] == 5u * 3u + x + y * 100u); + UNSCOPED_INFO("the image"); + REQUIRE(held[i] == values[i] * 3u + x + y * 100u); } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp index 68d6ae21fe..5ed6e330eb 100644 --- a/indra/llrender/tests/alinfiniteprojection_gl_test.cpp +++ b/indra/llrender/tests/alinfiniteprojection_gl_test.cpp @@ -34,16 +34,15 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include +#include "altestmatchers.h" #include #include // Numbers and bounds from an independent double-precision reference of the projection. -namespace tut +namespace { - namespace - { constexpr S32 SIZE = 8; constexpr F32 NEAR_PLANE = 0.1f; constexpr F32 FOV = 1.0f; @@ -128,12 +127,16 @@ namespace tut { if (!gGLManager.mHasClipControl) { - skip("no clip control: reverse-Z is unavailable on this context"); + SKIP("no clip control: reverse-Z is unavailable on this context"); } const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram = glCreateProgram(); glAttachShader(mProgram, vs); glAttachShader(mProgram, fs); @@ -142,7 +145,7 @@ namespace tut glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - ensure("program links", linked == GL_TRUE); + REQUIRE(linked == GL_TRUE); glGenVertexArrays(1, &mVAO); glGenTextures(1, &mDepth); @@ -156,7 +159,7 @@ namespace tut glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, mDepth, 0); glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); - ensure_equals("complete", (U32)glCheckFramebufferStatus(GL_FRAMEBUFFER), (U32)GL_FRAMEBUFFER_COMPLETE); + REQUIRE((U32)glCheckFramebufferStatus(GL_FRAMEBUFFER) == (U32)GL_FRAMEBUFFER_COMPLETE); glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); glViewport(0, 0, SIZE, SIZE); @@ -195,74 +198,52 @@ namespace tut GLuint mProgram = 0; }; - typedef test_group alinfiniteprojection_t; - typedef alinfiniteprojection_t::object alinfiniteprojection_object_t; - tut::alinfiniteprojection_t tut_alinfiniteprojection("alinfiniteprojection_gl"); - } // The matrix: recognised as infinite, stores near / distance, and its inverse gives w = 0 at the cleared // depth, which the reconstruction floor turns into MAX_RECONSTRUCT_DISTANCE. Finite and forward matrices are not. - template<> template<> - void alinfiniteprojection_object_t::test<1>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the infinite matrix stores near over distance and its far end is floored", "[alinfiniteprojection_gl]") { const LLMatrix4a proj = infinite(); - ensure("infinite", al_projection_is_infinite(proj)); - ensure("finite reversed is not", !al_projection_is_infinite(al_perspective(FOV, 1.f, NEAR_PLANE, 1024.f))); - ensure("ortho is not", !al_projection_is_infinite(al_ortho(-1.f, 1.f, -1.f, 1.f, 0.f, 10.f))); + CHECK(al_projection_is_infinite(proj)); + UNSCOPED_INFO("finite reversed is not"); + CHECK_FALSE(al_projection_is_infinite(al_perspective(FOV, 1.f, NEAR_PLANE, 1024.f))); + CHECK_FALSE(al_projection_is_infinite(al_ortho(-1.f, 1.f, -1.f, 1.f, 0.f, 10.f))); for (F32 dist : DISTANCES) { + CAPTURE(dist); LLVector4a clip; proj.transform4(LLVector4a(0.f, 0.f, -dist, 1.f), clip); - ensure_equals("clip z is the near plane", clip[2], NEAR_PLANE); - ensure_equals("clip w is the distance", clip[3], dist); + REQUIRE(clip[2] == NEAR_PLANE); + REQUIRE(clip[3] == dist); } LLMatrix4a inv = proj; inv.invert(); LLVector4a far_point; inv.transform4(LLVector4a(0.f, 0.f, 0.f, 1.f), far_point); - ensure("cleared depth unprojects to w = 0", fabsf(far_point[3]) < 1e-12f); - ensure_approximately_equals("floored to MAX_RECONSTRUCT_DISTANCE", reconstruct(inv, 0.f), MAX_RECONSTRUCT_DISTANCE, 8); + UNSCOPED_INFO("cleared depth unprojects to w = 0"); + CHECK(fabsf(far_point[3]) < 1e-12f); + CHECK_THAT(reconstruct(inv, 0.f), ll_test::WithinBits(MAX_RECONSTRUCT_DISTANCE, 8)); // The floor leaves a finite projection alone: its cleared depth still reconstructs at its far plane. LLMatrix4a inv_finite = al_perspective(FOV, 1.f, NEAR_PLANE, 1024.f); inv_finite.invert(); LLVector4a raw; inv_finite.transform4(LLVector4a(0.f, 0.f, 0.f, 1.f), raw); - ensure_equals("finite far untouched by the floor", reconstruct(inv_finite, 0.f), -raw[2] / raw[3]); - ensure("finite far is about 1024 m", fabsf(-raw[2] / raw[3] - 1024.f) < 1.f); + CHECK(reconstruct(inv_finite, 0.f) == -raw[2] / raw[3]); + CHECK(fabsf(-raw[2] / raw[3] - 1024.f) < 1.f); LLRender::sReverseZ = false; const LLMatrix4a forward = al_perspective(FOV, 1.f, NEAR_PLANE, std::numeric_limits::infinity()); - ensure("forward falls back to a finite plane", !al_projection_is_infinite(forward) && std::isfinite(forward.getColumn<2>()[2])); + CHECK_FALSE(al_projection_is_infinite(forward)); + CHECK(std::isfinite(forward.getColumn<2>()[2])); LLRender::sReverseZ = true; } - // Forward-Z cannot hold an infinite plane in 24-bit depth, so an infinite request falls back to the old - // fixed plane rather than MAX_FAR_PLANE, and depth beyond it is clipped as before. - template<> template<> - void alinfiniteprojection_object_t::test<4>() - { - LLRender::sReverseZ = false; - const LLMatrix4a forward = al_perspective(FOV, 1.f, NEAR_PLANE, std::numeric_limits::infinity()); - const LLMatrix4a legacy = al_perspective(FOV, 1.f, NEAR_PLANE, FINITE_PROJECTION_FAR); - LLRender::sReverseZ = true; - for (S32 i = 0; i < 16; ++i) - { - ensure_equals("same as the old fixed plane", forward.getF32ptr()[i], legacy.getF32ptr()[i]); - } - LLVector4a clip; - forward.transform4(LLVector4a(0.f, 0.f, -FINITE_PROJECTION_FAR, 1.f), clip); - ensure("far plane at the old fixed plane", fabsf(clip[2] / clip[3] - 1.f) < 1e-5f); - forward.transform4(LLVector4a(0.f, 0.f, -2.f * FINITE_PROJECTION_FAR, 1.f), clip); - ensure("past it is clipped", clip[2] / clip[3] > 1.f); - } - // Draw at each distance: the stored depth is near / distance (oracle), nothing is clipped far, and the // reconstructed distance is within the oracle's bound (0.29 mm, 3.9 mm, 9.3 mm, 45 mm). - template<> template<> - void alinfiniteprojection_object_t::test<2>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: drawn depth is near over distance and round-trips within the bound", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); @@ -273,33 +254,35 @@ namespace tut for (size_t i = 0; i < std::size(DISTANCES); ++i) { const F32 dist = DISTANCES[i]; - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); const F32 d = centreDepth(); - ensure("drawn at " + at, d > 0.f); - ensure("stored depth at " + at, fabsf(d - oracle_depth[i]) <= oracle_depth[i] * 1e-6f); + UNSCOPED_INFO("drawn"); + REQUIRE(d > 0.f); + REQUIRE(fabsf(d - oracle_depth[i]) <= oracle_depth[i] * 1e-6f); const F64 err = fabs((F64)reconstruct(inv, d) - dist); - ensure("round trip at " + at + ": " + std::to_string(err * 1000.0) + " mm", err <= bound(dist, d)); + UNSCOPED_INFO("round trip error " << err * 1000.0 << " mm"); + REQUIRE(err <= bound(dist, d)); } // Past every reach and still not clipped: 1000 km. glClear(GL_DEPTH_BUFFER_BIT); draw(proj, 1.0e6f); - ensure("1000 km is not clipped", centreDepth() > 0.f); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("1000 km is not clipped"); + CHECK(centreDepth() > 0.f); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // Two surfaces four depth steps apart at each distance: the nearer one wins whichever is drawn first. - template<> template<> - void alinfiniteprojection_object_t::test<3>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: surfaces four depth steps apart resolve in either draw order", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); for (F32 dist : DISTANCES) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); const F32 gap = (F32)(4.0 * depthStep(NEAR_PLANE / dist)) + (std::nextafter(dist, 2.f * dist) - dist); const F32 nearer = dist - gap; @@ -308,21 +291,44 @@ namespace tut const F32 d_far = centreDepth(); draw(proj, nearer); const F32 d_near = centreDepth(); - ensure("nearer drawn second wins at " + at, d_near > d_far); + UNSCOPED_INFO("nearer drawn second wins"); + REQUIRE(d_near > d_far); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, nearer); draw(proj, dist); - ensure_equals("farther drawn second loses at " + at, centreDepth(), d_near); + UNSCOPED_INFO("farther drawn second loses"); + REQUIRE(centreDepth() == d_near); } - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // Forward-Z cannot hold an infinite plane in 24-bit depth, so an infinite request falls back to the old + // fixed plane rather than MAX_FAR_PLANE, and depth beyond it is clipped as before. + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: forward-Z falls back to the old fixed plane and clips past it", "[alinfiniteprojection_gl]") + { + LLRender::sReverseZ = false; + const LLMatrix4a forward = al_perspective(FOV, 1.f, NEAR_PLANE, std::numeric_limits::infinity()); + const LLMatrix4a legacy = al_perspective(FOV, 1.f, NEAR_PLANE, FINITE_PROJECTION_FAR); + LLRender::sReverseZ = true; + for (S32 i = 0; i < 16; ++i) + { + CAPTURE(i); + REQUIRE(forward.getF32ptr()[i] == legacy.getF32ptr()[i]); + } + LLVector4a clip; + forward.transform4(LLVector4a(0.f, 0.f, -FINITE_PROJECTION_FAR, 1.f), clip); + UNSCOPED_INFO("far plane at the old fixed plane"); + CHECK(fabsf(clip[2] / clip[3] - 1.f) < 1e-5f); + forward.transform4(LLVector4a(0.f, 0.f, -2.f * FINITE_PROJECTION_FAR, 1.f), clip); + UNSCOPED_INFO("past it is clipped"); + CHECK(clip[2] / clip[3] > 1.f); } // The sky draws last, on the far plane, writing no depth, and the depth test spares it every pixel the world // covered (skyV.glsl). Under the infinite projection the far plane is the cleared depth exactly and geometry as // far out as reconstruction reaches still stores more, so the sky passes where nothing was drawn and nowhere else. - template<> template<> - void alinfiniteprojection_object_t::test<5>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the sky on the far plane passes only where nothing was drawn", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); @@ -343,30 +349,34 @@ namespace tut }; glClear(GL_DEPTH_BUFFER_BIT); - ensure_equals("the sky fills an empty frame", sky_samples(), (GLuint)(SIZE * SIZE)); - ensure_equals("the sky leaves the cleared depth", centreDepth(), 0.f); + UNSCOPED_INFO("the sky fills an empty frame"); + CHECK(sky_samples() == (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("the sky leaves the cleared depth"); + CHECK(centreDepth() == 0.f); for (F32 dist : { 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - ensure("geometry at " + at + " stores more than the far plane", centreDepth() > 0.f); - ensure_equals("the sky stays behind geometry at " + at, sky_samples(), (GLuint)0); + UNSCOPED_INFO("geometry stores more than the far plane"); + REQUIRE(centreDepth() > 0.f); + UNSCOPED_INFO("the sky stays behind geometry"); + REQUIRE(sky_samples() == (GLuint)0); } glDeleteQueries(1, &query); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // The haze pass draws on the far plane too (hazeV.glsl), testing the other way (LLPipeline::doAtmospherics): only // in front of the far plane, so it shades every pixel geometry covers, however far out, and no pixel of sky. - template<> template<> - void alinfiniteprojection_object_t::test<6>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the haze pass on the far plane shades all geometry and no sky", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); - ensure_equals("in front of the far plane is LESS under reverse-Z", (U32)LLGLDepthTest::remap(GL_GREATER), (U32)GL_LESS); + UNSCOPED_INFO("in front of the far plane is LESS under reverse-Z"); + CHECK((U32)LLGLDepthTest::remap(GL_GREATER) == (U32)GL_LESS); GLuint query = 0; glGenQueries(1, &query); const auto haze_samples = [&]() @@ -384,14 +394,16 @@ namespace tut }; glClear(GL_DEPTH_BUFFER_BIT); - ensure_equals("no haze on an empty frame", haze_samples(), (GLuint)0); + UNSCOPED_INFO("no haze on an empty frame"); + CHECK(haze_samples() == (GLuint)0); for (F32 dist : { 1.f, 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - ensure_equals("haze over all of the geometry at " + at, haze_samples(), (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("haze over all of the geometry"); + REQUIRE(haze_samples() == (GLuint)(SIZE * SIZE)); } // Pixel by pixel: geometry over the left half only, sky over the right. @@ -399,17 +411,17 @@ namespace tut glViewport(0, 0, SIZE / 2, SIZE); draw(proj, MAX_RECONSTRUCT_DISTANCE); glViewport(0, 0, SIZE, SIZE); - ensure_equals("haze over the geometry's half alone", haze_samples(), (GLuint)(SIZE * SIZE / 2)); + UNSCOPED_INFO("haze over the geometry's half alone"); + CHECK(haze_samples() == (GLuint)(SIZE * SIZE / 2)); glDeleteQueries(1, &query); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // The soften pass draws on the far plane twice (softenLightV.glsl, LLPipeline::renderDeferredLighting): its lit // program in front of the far plane, as the haze does, and its sky program on it. Between them every pixel is // shaded once, by the program for what it is, however far out the geometry lies. - template<> template<> - void alinfiniteprojection_object_t::test<7>() + TEST_CASE_METHOD(alinfiniteprojection_data, "alinfiniteprojection_gl: the soften pass shades every pixel once as lit or as sky", "[alinfiniteprojection_gl]") { setUp(); const LLMatrix4a proj = infinite(); @@ -430,16 +442,20 @@ namespace tut }; glClear(GL_DEPTH_BUFFER_BIT); - ensure_equals("nothing lit on an empty frame", samples(GL_GREATER), (GLuint)0); - ensure_equals("all sky on an empty frame", samples(GL_EQUAL), (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("nothing lit on an empty frame"); + CHECK(samples(GL_GREATER) == (GLuint)0); + UNSCOPED_INFO("all sky on an empty frame"); + CHECK(samples(GL_EQUAL) == (GLuint)(SIZE * SIZE)); for (F32 dist : { 1.f, 1000.f, 100000.f, MAX_RECONSTRUCT_DISTANCE }) { - const std::string at = std::to_string((S32)dist) + " m"; + CAPTURE(dist); glClear(GL_DEPTH_BUFFER_BIT); draw(proj, dist); - ensure_equals("all lit over geometry at " + at, samples(GL_GREATER), (GLuint)(SIZE * SIZE)); - ensure_equals("no sky over geometry at " + at, samples(GL_EQUAL), (GLuint)0); + UNSCOPED_INFO("all lit over geometry"); + REQUIRE(samples(GL_GREATER) == (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("no sky over geometry"); + REQUIRE(samples(GL_EQUAL) == (GLuint)0); } // Pixel by pixel: geometry at the reconstruction floor over the left half only, sky over the right. @@ -449,10 +465,12 @@ namespace tut glViewport(0, 0, SIZE, SIZE); const GLuint lit = samples(GL_GREATER); const GLuint sky = samples(GL_EQUAL); - ensure_equals("lit over the geometry's half", lit, (GLuint)(SIZE * SIZE / 2)); - ensure_equals("every pixel shaded once", lit + sky, (GLuint)(SIZE * SIZE)); + UNSCOPED_INFO("lit over the geometry's half"); + CHECK(lit == (GLuint)(SIZE * SIZE / 2)); + UNSCOPED_INFO("every pixel shaded once"); + CHECK(lit + sky == (GLuint)(SIZE * SIZE)); glDeleteQueries(1, &query); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/alinstanceddraw_gl_test.cpp b/indra/llrender/tests/alinstanceddraw_gl_test.cpp index 1b5614b09d..7040910af5 100644 --- a/indra/llrender/tests/alinstanceddraw_gl_test.cpp +++ b/indra/llrender/tests/alinstanceddraw_gl_test.cpp @@ -33,12 +33,10 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { - namespace - { constexpr S32 W = 16; constexpr S32 H = 16; @@ -96,18 +94,24 @@ namespace tut { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kGridVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kGridFragment); - ensure("grid shaders compile", vs && fs); + { + INFO("grid shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram.mProgramObject = glCreateProgram(); mProgram.mName = "attribute-less grid (test)"; mProgram.attachObject(vs); mProgram.attachObject(fs); - ensure("grid program links", mProgram.mapAttributes()); + UNSCOPED_INFO("grid program links"); + REQUIRE(mProgram.mapAttributes()); glDeleteShader(vs); glDeleteShader(fs); - ensure("grid program maps", mProgram.mapUniforms()); - ensure_equals("the grid program has no vertex inputs", mProgram.mAttributeMask, 0U); + REQUIRE(mProgram.mapUniforms()); + UNSCOPED_INFO("the grid program has no vertex inputs"); + REQUIRE(mProgram.mAttributeMask == 0U); - ensure("target allocates", mTarget.allocate(W, H, GL_RGBA8)); + REQUIRE(mTarget.allocate(W, H, GL_RGBA8)); mTarget.bindTarget(); gGL.setClearColor(LLColor4::black); mTarget.clear(); @@ -148,36 +152,33 @@ namespace tut LLRenderTarget mTarget; }; - typedef test_group alinstanceddraw_t; - typedef alinstanceddraw_t::object alinstanceddraw_object_t; - tut::alinstanceddraw_t tut_alinstanceddraw("alinstanceddraw_gl"); - } // Three instances of a four-vertex strip cover the first three cells of the bottom row and nothing else. - template<> template<> - void alinstanceddraw_object_t::test<1>() + TEST_CASE_METHOD(alinstanceddraw_data, "alinstanceddraw_gl: three instanced strips cover the bottom row's first three cells only", "[alinstanceddraw_gl]") { setUp(); gGL.loadMatrix(grid(0.f)); gGL.drawArraysInstanced(LLRender::TRIANGLE_STRIP, 0, 4, 3); - ensure_equals("cells covered", covered(), 0x7U); + CHECK(covered() == 0x7U); } // The draw syncs the matrices: a projection loaded after one draw places the next. The raw draw, which does not // sync, still places it by the projection the first draw left in the block. - template<> template<> - void alinstanceddraw_object_t::test<2>() + TEST_CASE_METHOD(alinstanceddraw_data, "alinstanceddraw_gl: drawArraysInstanced syncs the matrices and a raw draw does not", "[alinstanceddraw_gl]") { setUp(); gGL.loadMatrix(grid(0.f)); gGL.drawArraysInstanced(LLRender::TRIANGLE_STRIP, 0, 4, 1); - ensure_equals("first draw on the bottom row", covered(), 0x1U); + UNSCOPED_INFO("first draw on the bottom row"); + CHECK(covered() == 0x1U); gGL.loadMatrix(grid(2.f)); glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 2); - ensure_equals("the raw draw lands where the last synced projection put it", covered(), 0x3U); + UNSCOPED_INFO("the raw draw lands where the last synced projection put it"); + CHECK(covered() == 0x3U); gGL.drawArraysInstanced(LLRender::TRIANGLE_STRIP, 0, 4, 2); - ensure_equals("the synced draw lands on the third row", covered(), 0x3U | 0x300U); + UNSCOPED_INFO("the synced draw lands on the third row"); + CHECK(covered() == (0x3U | 0x300U)); } } diff --git a/indra/llrender/tests/alrenderstate_gl_test.cpp b/indra/llrender/tests/alrenderstate_gl_test.cpp index d17807e860..6846753e9e 100644 --- a/indra/llrender/tests/alrenderstate_gl_test.cpp +++ b/indra/llrender/tests/alrenderstate_gl_test.cpp @@ -34,12 +34,10 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { - namespace - { constexpr U32 SIZE = 4; const LLColor4U RED(255, 0, 0, 255); @@ -84,39 +82,33 @@ namespace tut void setUp() { - ensure("target allocates", mTarget.allocate(SIZE, SIZE, GL_RGBA8)); + REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8)); } ll_test::SharedGLScope mGL; LLRenderTarget mTarget; }; - typedef test_group alrenderstate_t; - typedef alrenderstate_t::object alrenderstate_object_t; - tut::alrenderstate_t tut_alrenderstate("alrenderstate_gl"); - } // The clear colour a target's clear fills with is the one gGL was given, and GL holds what gGL says it does. - template<> template<> - void alrenderstate_object_t::test<1>() + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: a target's clear fills with the colour gGL was given", "[alrenderstate_gl]") { setUp(); mTarget.bindTarget(); gGL.setClearColor(LLColor4::red); - ensure("GL holds the cached colour", glClearValue() == gGL.getClearColor()); + CHECK(glClearValue() == gGL.getClearColor()); mTarget.clear(); - ensure_equals("cleared red", pixel(1, 1), RED); + CHECK(pixel(1, 1) == RED); gGL.setClearColor(LLColor4::green); mTarget.clear(); - ensure_equals("cleared green", pixel(1, 1), GREEN); + CHECK(pixel(1, 1) == GREEN); mTarget.flush(); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // refreshState is for state changed behind gGL's back: it puts back what gGL holds, where the setters would see // their cache agreeing and issue nothing. - template<> template<> - void alrenderstate_object_t::test<2>() + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: refreshState puts back the clear colour and scissor box gGL holds", "[alrenderstate_gl]") { gGL.setClearColor(LLColor4::green); gGL.setScissor(1, 1, 2, 2); @@ -125,54 +117,23 @@ namespace tut glScissor(0, 0, 1, 1); gGL.setClearColor(LLColor4::green); gGL.setScissor(1, 1, 2, 2); - ensure("an agreeing cache issues nothing", glClearValue() == LLColor4::red); + UNSCOPED_INFO("an agreeing cache issues nothing"); + CHECK(glClearValue() == LLColor4::red); gGL.refreshState(); - ensure("refreshState restores the clear colour", glClearValue() == LLColor4::green); + CHECK(glClearValue() == LLColor4::green); S32 box[4]; glScissorBox(box); - ensure("refreshState restores the scissor box", box[0] == 1 && box[1] == 1 && box[2] == 2 && box[3] == 2); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); - } - - // And the patch size, on a context with tessellation to have one. - template<> template<> - void alrenderstate_object_t::test<4>() - { - if (!glPatchParameteri) - { - skip("this context has no tessellation"); - } - - // Handed back however the test ends: the shared context outlives it, and its scope does not keep this. - struct PatchSizeRestore - { - GLint mFound = 0; - PatchSizeRestore() { glGetIntegerv(GL_PATCH_VERTICES, &mFound); } - ~PatchSizeRestore() - { - gGL.setPatchVertices((U32)mFound); - glPatchParameteri(GL_PATCH_VERTICES, mFound); - } - } restore; - - gGL.setPatchVertices(4); - glPatchParameteri(GL_PATCH_VERTICES, 3); - gGL.setPatchVertices(4); - GLint patch = 0; - glGetIntegerv(GL_PATCH_VERTICES, &patch); - ensure_equals("an agreeing cache issues nothing", (S32)patch, 3); - - gGL.refreshState(); - glGetIntegerv(GL_PATCH_VERTICES, &patch); - ensure_equals("refreshState restores the patch size", (S32)patch, 4); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK(box[0] == 1); + CHECK(box[1] == 1); + CHECK(box[2] == 2); + CHECK(box[3] == 2); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // LLGLSScissor cuts a clear to its box with the test on, and hands back the box and the test it found, nested // scopes included. - template<> template<> - void alrenderstate_object_t::test<3>() + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: LLGLSScissor cuts a clear to its box and hands back what it found", "[alrenderstate_gl]") { setUp(); mTarget.bindTarget(); @@ -182,28 +143,85 @@ namespace tut { LLGLSScissor left(0, 0, SIZE / 2, SIZE); - ensure("the test is on in the scope", glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); + UNSCOPED_INFO("the test is on in the scope"); + CHECK(glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); gGL.setClearColor(LLColor4::green); mTarget.clear(); { LLGLSScissor corner(0, 0, 1, 1); S32 box[4]; gGL.getScissor(box); - ensure("the inner scope's box", box[0] == 0 && box[1] == 0 && box[2] == 1 && box[3] == 1); + { + INFO("the inner scope's box"); + CHECK(box[0] == 0); + CHECK(box[1] == 0); + CHECK(box[2] == 1); + CHECK(box[3] == 1); + } } S32 box[4]; glScissorBox(box); - ensure("the outer box is back", box[0] == 0 && box[1] == 0 && box[2] == (S32)SIZE / 2 && box[3] == (S32)SIZE); - ensure("the test is still on", glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); + { + INFO("the outer box is back"); + CHECK(box[0] == 0); + CHECK(box[1] == 0); + CHECK(box[2] == (S32)SIZE / 2); + CHECK(box[3] == (S32)SIZE); + } + UNSCOPED_INFO("the test is still on"); + CHECK(glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE); } - ensure_equals("inside the box: cleared green", pixel(0, 1), GREEN); - ensure_equals("outside the box: still red", pixel(SIZE - 1, 1), RED); - ensure("the test is off again", glIsEnabled(GL_SCISSOR_TEST) == GL_FALSE); + UNSCOPED_INFO("inside the box: cleared green"); + CHECK(pixel(0, 1) == GREEN); + UNSCOPED_INFO("outside the box: still red"); + CHECK(pixel(SIZE - 1, 1) == RED); + UNSCOPED_INFO("the test is off again"); + CHECK(glIsEnabled(GL_SCISSOR_TEST) == GL_FALSE); S32 box[4]; glScissorBox(box); - ensure("the first box is back", box[0] == 0 && box[1] == 0 && box[2] == (S32)SIZE && box[3] == (S32)SIZE); + { + INFO("the first box is back"); + CHECK(box[0] == 0); + CHECK(box[1] == 0); + CHECK(box[2] == (S32)SIZE); + CHECK(box[3] == (S32)SIZE); + } mTarget.flush(); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); + } + + // refreshState puts back the patch size too, on a context with tessellation to have one. + TEST_CASE_METHOD(alrenderstate_data, "alrenderstate_gl: refreshState puts back the patch size gGL holds", "[alrenderstate_gl]") + { + if (!glPatchParameteri) + { + SKIP("this context has no tessellation"); + } + + // Handed back however the test ends: the shared context outlives it, and its scope does not keep this. + struct PatchSizeRestore + { + GLint mFound = 0; + PatchSizeRestore() { glGetIntegerv(GL_PATCH_VERTICES, &mFound); } + ~PatchSizeRestore() + { + gGL.setPatchVertices((U32)mFound); + glPatchParameteri(GL_PATCH_VERTICES, mFound); + } + } restore; + + gGL.setPatchVertices(4); + glPatchParameteri(GL_PATCH_VERTICES, 3); + gGL.setPatchVertices(4); + GLint patch = 0; + glGetIntegerv(GL_PATCH_VERTICES, &patch); + UNSCOPED_INFO("an agreeing cache issues nothing"); + CHECK((S32)patch == 3); + + gGL.refreshState(); + glGetIntegerv(GL_PATCH_VERTICES, &patch); + CHECK((S32)patch == 4); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/altessellation_gl_test.cpp b/indra/llrender/tests/altessellation_gl_test.cpp index 7e8073e894..db3f820e0a 100644 --- a/indra/llrender/tests/altessellation_gl_test.cpp +++ b/indra/llrender/tests/altessellation_gl_test.cpp @@ -34,16 +34,14 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { - namespace - { // The stages the terrain program has: a pass-through vertex shader, a // control shader that takes its levels from uniforms, an evaluation // shader that bilinearly places the quad, and a flat fragment shader. @@ -273,65 +271,68 @@ namespace tut U32 mPriorArrays = 0; }; - typedef test_group altessellation_test; - typedef altessellation_test::object altessellation_object; - tut::altessellation_test altessellation_testcase("ALTessellation"); - } // The context compiles both tessellation stages and links them with a // vertex and fragment shader. Everything below assumes this. - template<> template<> - void altessellation_object::test<1>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: the context compiles and links a four-stage tessellation program", "[ALTessellation]") { - ensure("all four stages compiled", mCompiled); - ensure("four-stage program linked", mLinked); - ensure("outer levels uniform found", mOuter >= 0); - ensure("inner levels uniform found", mInner >= 0); + CHECK(mCompiled); + CHECK(mLinked); + UNSCOPED_INFO("outer levels uniform found"); + CHECK(mOuter >= 0); + UNSCOPED_INFO("inner levels uniform found"); + CHECK(mInner >= 0); } // At level 1 everywhere a counter-clockwise patch is two triangles over // the whole framebuffer, and back-face culling keeps them. - template<> template<> - void altessellation_object::test<2>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: at level 1 a counter-clockwise patch covers the whole framebuffer", "[ALTessellation]") { - ensure("program linked", mLinked); + REQUIRE(mLinked); setLevels(1, 1, 1, 1, 1, 1); const std::vector px = draw(kCCW); - ensure("bottom-left covered", white(px, 1, 1)); - ensure("bottom-right covered", white(px, W - 2, 1)); - ensure("top-right covered", white(px, W - 2, H - 2)); - ensure("top-left covered", white(px, 1, H - 2)); - ensure("centre covered", white(px, W / 2, H / 2)); + UNSCOPED_INFO("bottom-left covered"); + CHECK(white(px, 1, 1)); + UNSCOPED_INFO("bottom-right covered"); + CHECK(white(px, W - 2, 1)); + UNSCOPED_INFO("top-right covered"); + CHECK(white(px, W - 2, H - 2)); + UNSCOPED_INFO("top-left covered"); + CHECK(white(px, 1, H - 2)); + UNSCOPED_INFO("centre covered"); + CHECK(white(px, W / 2, H / 2)); } // The same corners clockwise are back faces: `layout(ccw)` with u along // x and v along y is the winding the pipeline's cull expects. - template<> template<> - void altessellation_object::test<3>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: the same corners clockwise are culled as back faces", "[ALTessellation]") { - ensure("program linked", mLinked); + REQUIRE(mLinked); setLevels(1, 1, 1, 1, 1, 1); const std::vector px = draw(kCW); - ensure("bottom-left culled", black(px, 1, 1)); - ensure("top-right culled", black(px, W - 2, H - 2)); - ensure("centre culled", black(px, W / 2, H / 2)); + UNSCOPED_INFO("bottom-left culled"); + CHECK(black(px, 1, 1)); + UNSCOPED_INFO("top-right culled"); + CHECK(black(px, W - 2, H - 2)); + UNSCOPED_INFO("centre culled"); + CHECK(black(px, W / 2, H / 2)); } // Subdivided with fractional spacing the patch still covers every pixel: // no cracks between the pieces, at integer and at fractional levels. - template<> template<> - void altessellation_object::test<4>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: subdivided with fractional spacing the patch still covers every pixel", "[ALTessellation]") { - ensure("program linked", mLinked); + REQUIRE(mLinked); for (const F32 level : { 4.f, 2.5f, 7.25f }) { + CAPTURE(level); setLevels(level, level, level, level, level, level); const std::vector px = draw(kCCW); size_t uncovered = 0; for (S32 y = 0; y < H; ++y) for (S32 x = 0; x < W; ++x) if (!white(px, x, y)) ++uncovered; - ensure_equals("every pixel covered at level " + std::to_string(level), uncovered, size_t(0)); + REQUIRE(uncovered == size_t(0)); } } @@ -339,27 +340,25 @@ namespace tut // honours the levels and transform feedback sees what it emits. Odd // spacing rounds a level up to the next odd integer, so 3 is the smallest // level above 1 whose segments are all equal and whose count is exact. - template<> template<> - void altessellation_object::test<5>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: the tessellator emits two triangles at level 1 and eighteen at level 3", "[ALTessellation]") { - ensure("program linked", mLinked); + REQUIRE(mLinked); GLuint triangles = 0; setLevels(1, 1, 1, 1, 1, 1); capture(kCCW, triangles); - ensure_equals("level 1 is two triangles", triangles, GLuint(2)); + CHECK(triangles == GLuint(2)); setLevels(3, 3, 3, 3, 3, 3); capture(kCCW, triangles); - ensure_equals("level 3 is eighteen triangles", triangles, GLuint(18)); + CHECK(triangles == GLuint(18)); } // Which outer level drives which edge. Raising one outer level to 3 puts // four distinct vertices on exactly one side of the patch and leaves two // on every other. Outer[0] is the u = 0 edge (x = -1), [1] the v = 0 edge // (y = -1), [2] u = 1 (x = +1), [3] v = 1 (y = +1). - template<> template<> - void altessellation_object::test<6>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: each outer level splits only its own edge of the patch", "[ALTessellation]") { - ensure("program linked", mLinked); + REQUIRE(mLinked); // Edge i of the patch, as outer[i] should see it. struct Edge { int axis; F32 value; const char* name; }; const Edge edges[4] = { @@ -377,26 +376,27 @@ namespace tut const std::vector verts = capture(kCCW, triangles); for (int i = 0; i < 4; ++i) { + CAPTURE(i); const size_t expected = i == raised ? 4 : 2; - ensure_equals(std::string(edges[raised].name) + (i == raised ? "" : ": other side unsplit"), - distinctOn(verts, edges[i].axis, edges[i].value), expected); + UNSCOPED_INFO(edges[raised].name << (i == raised ? "" : ": other side unsplit")); + REQUIRE(distinctOn(verts, edges[i].axis, edges[i].value) == expected); } } } // Profiling a tessellated draw must still render. Apple's Metal-backed // OpenGL driver aborts when GL_PRIMITIVES_GENERATED is active here. - template<> template<> - void altessellation_object::test<7>() + TEST_CASE_METHOD(altessellation_data, "ALTessellation: a profiled tessellated draw still renders", "[ALTessellation]") { - ensure("program linked", mLinked); + REQUIRE(mLinked); // Viewer programs do not capture transform feedback. Declaring a // captured varying takes a different Apple driver path and hides the crash. glTransformFeedbackVaryings(mProgram, 0, nullptr, GL_INTERLEAVED_ATTRIBS); glLinkProgram(mProgram); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - ensure("program linked without transform feedback", linked == GL_TRUE); + UNSCOPED_INFO("program linked without transform feedback"); + REQUIRE(linked == GL_TRUE); mOuter = glGetUniformLocation(mProgram, "outer"); mInner = glGetUniformLocation(mProgram, "inner"); setLevels(3, 3, 3, 3, 3, 3); @@ -410,9 +410,12 @@ namespace tut LLGLSLShader::sProfileEnabled = false; const GLuint queries[] = { profile.mTimerQuery, profile.mSamplesQuery, profile.mPrimitivesQuery }; glDeleteQueries(3, queries); - ensure("profile read completed", read); - ensure("profiled terrain covers centre", white(px, W / 2, H / 2)); - ensure("sample query still counts fragments", profile.mSamplesDrawn > 0); - ensure_equals("profiling leaves no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("profile read completed"); + CHECK(read); + UNSCOPED_INFO("profiled terrain covers centre"); + CHECK(white(px, W / 2, H / 2)); + UNSCOPED_INFO("sample query still counts fragments"); + CHECK(profile.mSamplesDrawn > 0); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/alwindowsdlheadless_test.cpp b/indra/llrender/tests/alwindowsdlheadless_test.cpp index 145ec90e06..41a57d81c2 100644 --- a/indra/llrender/tests/alwindowsdlheadless_test.cpp +++ b/indra/llrender/tests/alwindowsdlheadless_test.cpp @@ -31,12 +31,10 @@ #include "llwindow.h" -#include "../test/lltut.h" +#include #include -namespace tut -{ namespace { struct alwindowsdlheadless_data @@ -53,41 +51,35 @@ namespace alwindowsdlheadless_data() { gl(); } }; - typedef test_group alwindowsdlheadless_test; - typedef alwindowsdlheadless_test::object alwindowsdlheadless_object; - tut::alwindowsdlheadless_test alwindowsdlheadless_testcase("ALWindowSDLHeadless"); -} // The window the manager handed the fixture is the hidden backend, valid, // with an SDL window behind it and the size that was asked for. - template<> template<> - void alwindowsdlheadless_object::test<1>() + TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: the fixture's window is the hidden backend at the size asked for", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); - ensure("a window", window != nullptr); - ensure("the hidden backend was asked for", - LLWindowManager::getBackend() == ALWindowBackend::Hidden); + REQUIRE(window != nullptr); + CHECK(LLWindowManager::getBackend() == ALWindowBackend::Hidden); // createWindow would have returned null on an invalid one; what is // left to see is the SDL window behind it. - ensure("there is an SDL window behind it", window->getPlatformWindow() != nullptr); + UNSCOPED_INFO("there is an SDL window behind it"); + REQUIRE(window->getPlatformWindow() != nullptr); LLCoordWindow size; - ensure("the size can be read", window->getSize(&size)); - ensure_equals("width as asked", size.mX, ll_test::HeadlessGL::WIDTH); - ensure_equals("height as asked", size.mY, ll_test::HeadlessGL::HEIGHT); + REQUIRE(window->getSize(&size)); + CHECK(size.mX == ll_test::HeadlessGL::WIDTH); + CHECK(size.mY == ll_test::HeadlessGL::HEIGHT); } // A worker thread's shared context, as LLImageGLThread and the VBO // streaming thread use it: made on the main thread, bound and released // on the worker, and the texture the worker makes on it is the same // object on the main thread's context afterwards. - template<> template<> - void alwindowsdlheadless_object::test<2>() + TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: a texture made on a worker's shared context is the main one's too", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); void* shared = window->createSharedContext(); - ensure("a shared context was made", shared != nullptr); + REQUIRE(shared != nullptr); GLuint name = 0; GLenum worker_error = GL_NO_ERROR; @@ -109,9 +101,11 @@ namespace }); worker.join(); - ensure_equals("the worker's GL calls raised no error", worker_error, (GLenum)GL_NO_ERROR); - ensure("the worker made a texture", name != 0); - ensure("the main context sees the worker's texture", glIsTexture(name) == GL_TRUE); + CHECK(worker_error == (GLenum)GL_NO_ERROR); + UNSCOPED_INFO("the worker made a texture"); + CHECK(name != 0); + UNSCOPED_INFO("the main context sees the worker's texture"); + CHECK(glIsTexture(name) == GL_TRUE); // The shared context's texture slots cache what they bound, so the // read puts back the texture it found. @@ -121,20 +115,21 @@ namespace GLint width = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); glBindTexture(GL_TEXTURE_2D, prior); - ensure_equals("with the storage the worker gave it", width, 1); - ensure_equals("and no error on this side", glGetError(), (GLenum)GL_NO_ERROR); + UNSCOPED_INFO("with the storage the worker gave it"); + CHECK(width == 1); + UNSCOPED_INFO("no error on the main context"); + CHECK(glGetError() == (GLenum)GL_NO_ERROR); glDeleteTextures(1, &name); } - // A shared context nobody bound is still a shared context to destroy. - template<> template<> - void alwindowsdlheadless_object::test<3>() + TEST_CASE_METHOD(alwindowsdlheadless_data, "ALWindowSDLHeadless: a shared context nobody bound can still be destroyed", "[ALWindowSDLHeadless]") { LLWindow* window = gl().window(); void* shared = window->createSharedContext(); - ensure("a shared context was made", shared != nullptr); + REQUIRE(shared != nullptr); window->destroySharedContext(shared); - ensure_equals("the main context is untouched", glGetError(), (GLenum)GL_NO_ERROR); + UNSCOPED_INFO("the main context is untouched"); + CHECK(glGetError() == (GLenum)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/llfontbitmapcache_gl_test.cpp b/indra/llrender/tests/llfontbitmapcache_gl_test.cpp index de3ca03b41..ccabdaa6f3 100644 --- a/indra/llrender/tests/llfontbitmapcache_gl_test.cpp +++ b/indra/llrender/tests/llfontbitmapcache_gl_test.cpp @@ -31,10 +31,8 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include -namespace tut -{ namespace { using namespace ll_test; @@ -48,133 +46,121 @@ namespace std::unique_ptr gl = std::make_unique(); }; - typedef test_group llfontbitmapcache_gl_test; - typedef llfontbitmapcache_gl_test::object llfontbitmapcache_gl_object; - tut::llfontbitmapcache_gl_test llfontbitmapcache_gl_testcase("LLFontBitmapCacheGL"); -} // After a Grayscale allocation, the underlying LLImageRaw must // carry 2 components (R+G with R=255 stem, G=alpha) — pins // c99dcaa7d3 which restored the greyscale atlas to 2 components // after a regression had it at 1. - template<> template<> - void llfontbitmapcache_gl_object::test<1>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a Grayscale sheet has 2 components", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; - ensure("nextOpenPos succeeded", - c.nextOpenPos(/*width=*/8, /*height=*/8, px, py, EFontGlyphType::Grayscale, sheet)); + CHECK(c.nextOpenPos(/*width=*/8, /*height=*/8, px, py, EFontGlyphType::Grayscale, sheet)); LLImageRaw* raw = c.getImageRaw(EFontGlyphType::Grayscale, sheet); - ensure("raw image present", raw != nullptr); - ensure_equals("Grayscale atlas has 2 components", - (S32)raw->getComponents(), 2); + REQUIRE(raw != nullptr); + CHECK((S32)raw->getComponents() == 2); } // Color allocation must produce a 4-component (RGBA) raw — pins // the same array-init bug from c99dcaa7d3 in its Color form. - template<> template<> - void llfontbitmapcache_gl_object::test<2>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a Color sheet has 4 components", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; - ensure("nextOpenPos succeeded", - c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet)); + CHECK(c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet)); LLImageRaw* raw = c.getImageRaw(EFontGlyphType::Color, sheet); - ensure("raw image present", raw != nullptr); - ensure_equals("Color atlas has 4 components", - (S32)raw->getComponents(), 4); + REQUIRE(raw != nullptr); + CHECK((S32)raw->getComponents() == 4); } // The very first nextOpenPos on a fresh cache must yield (4, 4) — // pins the mCurrentOffsetX/Y = {4, 4} array-init in the header // (an earlier `= { 4 }` form was zero-initing index [1], which // started Color atlases at offset 0). - template<> template<> - void llfontbitmapcache_gl_object::test<3>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: the first Grayscale and Color allocations start past the 4px border", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = -1, py = -1; U32 sheet = 99; - ensure("first Grayscale alloc succeeded", - c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet)); - ensure_equals("first Grayscale posX == 4", px, 4); - ensure_equals("first Grayscale posY == 4", py, 4); - ensure_equals("first Grayscale sheet index == 0", (S32)sheet, 0); + CHECK(c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet)); + CHECK(px == 4); + CHECK(py == 4); + CHECK((S32)sheet == 0); // Repeat for Color — a fresh cache, fresh allocation. The bug // would have started Color at posX=0; the fix shows posX=4. LLFontBitmapCache c2; c2.init(2, 2); S32 px2 = -1, py2 = -1; U32 sheet2 = 99; - ensure("first Color alloc succeeded", - c2.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Color, sheet2)); - ensure_equals("first Color posX == 4", px2, 4); - ensure_equals("first Color posY == 4", py2, 4); + CHECK(c2.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Color, sheet2)); + CHECK(px2 == 4); + CHECK(py2 == 4); } // Sequential allocations within the same row advance posX by // width + 4 (4-pixel border). Tests the row-packing pen advance // at llfontbitmapcache.cpp:201. - template<> template<> - void llfontbitmapcache_gl_object::test<4>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: allocations in a row advance x by the width and the 4px border", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(10, 8, px, py, EFontGlyphType::Grayscale, sheet); - ensure_equals("first posX", px, 4); - ensure_equals("first posY", py, 4); + CHECK(px == 4); + CHECK(py == 4); c.nextOpenPos(10, 8, px, py, EFontGlyphType::Grayscale, sheet); // Expected: 4 (initial border) + 10 (first width) + 4 (border) - ensure_equals("second posX advances by width+4", px, 18); - ensure_equals("second posY stays in same row", py, 4); + UNSCOPED_INFO("the second allocation advances by width + 4"); + CHECK(px == 18); + UNSCOPED_INFO("in the same row"); + CHECK(py == 4); } // When the next allocation would exceed mBitmapWidth, the row // wraps. posX resets to 4; posY advances by the tallest glyph // placed in the prior row (mCurrentRowMaxHeight, not mMaxCharHeight). - template<> template<> - void llfontbitmapcache_gl_object::test<5>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a full row wraps below the tallest glyph placed in it", "[LLFontBitmapCacheGL]") { // 128x128 atlas. Two 50px-wide glyphs fit in row 0 // (4 + 50 + 4 + 50 + 4 = 112 ≤ 128); third forces wrap. LLFontBitmapCache c; c.init(2, 2); - ensure_equals("sheet width is 128", c.getBitmapWidth(), 128); + CHECK(c.getBitmapWidth() == 128); S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); // row 0, col 0 c.nextOpenPos(50, 20, px, py, EFontGlyphType::Grayscale, sheet); // row 0, col 1 - ensure_equals("second alloc still in row 0", py, 4); + UNSCOPED_INFO("the second allocation is still in row 0"); + CHECK(py == 4); c.nextOpenPos(50, 8, px, py, EFontGlyphType::Grayscale, sheet); // wraps to row 1 - ensure_equals("third alloc wraps posX to 4", px, 4); + UNSCOPED_INFO("the third allocation wraps"); + CHECK(px == 4); // Tallest in row 0 was 30, so row 1 starts at 4 + 30 + 4 = 38. - ensure_equals("third alloc posY uses tallest glyph in prior row", - py, 38); - ensure_equals("still on sheet 0 (row wrap, not sheet rollover)", - (S32)sheet, 0); + UNSCOPED_INFO("below the tallest glyph of row 0"); + CHECK(py == 38); + UNSCOPED_INFO("a row wrap and not a new sheet"); + CHECK((S32)sheet == 0); } // When a row wrap would push posY past mBitmapHeight, the cache // must allocate a fresh sheet rather than write past the atlas. // Verify by repeatedly forcing wraps until the next would overflow. - template<> template<> - void llfontbitmapcache_gl_object::test<6>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a row wrap that would pass the bottom starts a new sheet", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); - ensure_equals("sheet width is 128", c.getBitmapWidth(), 128); - ensure_equals("sheet height is 128", c.getBitmapHeight(), 128); + CHECK(c.getBitmapWidth() == 128); + CHECK(c.getBitmapHeight() == 128); // Glyphs 50 wide, 30 tall. 2 per row (positions 4 and 58), // 3 rows (Y = 4, 38, 72) before next would land at Y=106 @@ -184,28 +170,28 @@ namespace { c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); } - ensure_equals("six allocs of 50x30 still on sheet 0", - (S32)sheet, 0); - ensure_equals("one sheet so far", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); + UNSCOPED_INFO("six allocations fill three rows of sheet 0"); + CHECK((S32)sheet == 0); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); // 7th glyph: Y advance to 4+30+4+30+4+30+4=106, bottom would // be 106+30+4=140 > 128 → new sheet. Either the row-wrap path // (line 125) or the vertical-fit recheck (line 144) fires. c.nextOpenPos(50, 30, px, py, EFontGlyphType::Grayscale, sheet); - ensure_equals("7th alloc moved to new sheet", - (S32)sheet, 1); - ensure_equals("two sheets allocated", - c.getNumBitmaps(EFontGlyphType::Grayscale), 2u); - ensure_equals("new sheet starts at posX=4", px, 4); - ensure_equals("new sheet starts at posY=4", py, 4); + UNSCOPED_INFO("the seventh allocation goes to a new sheet"); + CHECK((S32)sheet == 1); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 2u); + { + INFO("the new sheet's first position"); + CHECK(px == 4); + CHECK(py == 4); + } } // Per-type isolation: a Grayscale allocation must not move the // Color pen, and vice versa. Each type owns its own X/Y/row-max // state at index [bitmap_idx]. - template<> template<> - void llfontbitmapcache_gl_object::test<7>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: Grayscale and Color pens and sheets are independent", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -217,21 +203,21 @@ namespace // Color must still be at the (4, 4) start. S32 cpx = 0, cpy = 0; U32 csheet = 99; c.nextOpenPos(8, 8, cpx, cpy, EFontGlyphType::Color, csheet); - ensure_equals("Color pen unaffected by Grayscale activity (x)", cpx, 4); - ensure_equals("Color pen unaffected by Grayscale activity (y)", cpy, 4); - ensure_equals("Color sheet index starts at 0", (S32)csheet, 0); - ensure_equals("Grayscale + Color each have one sheet", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure_equals("Color has its own sheet vector", - c.getNumBitmaps(EFontGlyphType::Color), 1u); + { + INFO("the Color pen after two Grayscale allocations"); + CHECK(cpx == 4); + CHECK(cpy == 4); + } + CHECK((S32)csheet == 0); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + CHECK(c.getNumBitmaps(EFontGlyphType::Color) == 1u); } // Generation counter must strictly increase on every allocation // and on releaseSheet. Vertex buffers compare against a captured // generation to invalidate; equal-or-decreasing generations would // mask the invalidation. - template<> template<> - void llfontbitmapcache_gl_object::test<8>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: the generation rises on allocation and release and reset", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -240,30 +226,28 @@ namespace S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); const S32 g1 = c.getCacheGeneration(); - ensure("generation bumped by nextOpenPos", g1 > g0); + CHECK(g1 > g0); c.releaseSheet(EFontGlyphType::Grayscale, sheet); const S32 g2 = c.getCacheGeneration(); - ensure("generation bumped by releaseSheet", g2 > g1); + CHECK(g2 > g1); c.reset(); const S32 g3 = c.getCacheGeneration(); - ensure("generation bumped by reset", g3 > g2); + CHECK(g3 > g2); } // releaseSheet drops the LLImageRaw + LLImageGL but keeps the // slot index alive (as nullptr) so existing index references stay // valid. isSheetReleased flips true; getSheetLastUsedTime returns 0. - template<> template<> - void llfontbitmapcache_gl_object::test<9>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: releaseSheet frees the images and keeps the slot", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 sheet = 0; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); - ensure("sheet not yet released", - !c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); // Don't assert getSheetLastUsedTime is > 0 here — it samples // LLFrameTimer::getTotalSeconds() and the test environment may // not have ticked a frame yet, so the wall-clock can legitimately @@ -273,15 +257,12 @@ namespace c.releaseSheet(EFontGlyphType::Grayscale, sheet); - ensure("isSheetReleased flips true after release", - c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); - ensure_equals("released sheet getImageRaw returns null", - c.getImageRaw(EFontGlyphType::Grayscale, sheet), - (LLImageRaw*)nullptr); - ensure_equals("released sheet last-used time cleared to 0", - c.getSheetLastUsedTime(EFontGlyphType::Grayscale, sheet), 0.0); - ensure_equals("getNumBitmaps preserves slot count after release", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); + CHECK(c.isSheetReleased(EFontGlyphType::Grayscale, sheet)); + CHECK(c.getImageRaw(EFontGlyphType::Grayscale, sheet) == nullptr); + UNSCOPED_INFO("a released sheet's last-used time is cleared"); + CHECK(c.getSheetLastUsedTime(EFontGlyphType::Grayscale, sheet) == 0.0); + UNSCOPED_INFO("the slot is kept"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); } // After releasing the active sheet, the next nextOpenPos must build a @@ -291,8 +272,7 @@ namespace // index, so reuse is safe and bounds slot growth across eviction // cycles. Pins the active_sheet_released check + slot recycling in // nextOpenPos. - template<> template<> - void llfontbitmapcache_gl_object::test<10>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: an allocation after the active sheet's release rebuilds it in that slot", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -301,16 +281,41 @@ namespace c.releaseSheet(EFontGlyphType::Grayscale, sheet); S32 px2 = 0, py2 = 0; U32 sheet2 = 99; - ensure("nextOpenPos succeeds after active sheet release", - c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2)); - ensure_equals("released slot 0 is recycled for the new sheet", - (S32)sheet2, 0); - ensure_equals("new sheet starts at posX=4", px2, 4); - ensure_equals("new sheet starts at posY=4", py2, 4); - ensure("recycled slot 0 is live again", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); - ensure_equals("slot vector did not grow", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); + CHECK(c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2)); + UNSCOPED_INFO("the released slot is recycled"); + CHECK((S32)sheet2 == 0); + { + INFO("the new sheet's first position"); + CHECK(px2 == 4); + CHECK(py2 == 4); + } + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + } + + // reset() after allocations clears all state — no live sheets, + // pens back at 4, and a subsequent nextOpenPos starts from (4, 4) + // on a fresh sheet. + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: reset clears every sheet and the pen starts over", "[LLFontBitmapCacheGL]") + { + LLFontBitmapCache c; + c.init(2, 2); + S32 px = 0, py = 0; U32 sheet = 0; + c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); + c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); + UNSCOPED_INFO("two glyphs on one sheet before reset"); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + + c.reset(); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 0u); + CHECK(c.getNumBitmaps(EFontGlyphType::Color) == 0u); + + // Re-init and verify pens really did reset back to (4,4). + c.init(2, 2); + S32 px2 = -1, py2 = -1; U32 sheet2 = 99; + c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2); + CHECK(px2 == 4); + CHECK(py2 == 4); } // Multi-cycle release+re-alloc: alloc, release, alloc, release, alloc @@ -318,40 +323,36 @@ namespace // cycles, and the generation counter still advances every cycle so // captured vertex buffers rebuild. Pins bounded slot growth under // repeat cycles. - template<> template<> - void llfontbitmapcache_gl_object::test<12>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: repeated release and allocation keeps recycling slot 0", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); S32 px = 0, py = 0; U32 s0 = 0, s1 = 99, s2 = 99; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s0); - ensure_equals("first alloc lands on sheet 0", (S32)s0, 0); + CHECK((S32)s0 == 0); const S32 gen0 = c.getCacheGeneration(); c.releaseSheet(EFontGlyphType::Grayscale, s0); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s1); - ensure_equals("second alloc recycles slot 0", (S32)s1, 0); + CHECK((S32)s1 == 0); const S32 gen1 = c.getCacheGeneration(); - ensure("generation advanced across release+recycle", gen1 > gen0); + CHECK(gen1 > gen0); c.releaseSheet(EFontGlyphType::Grayscale, s1); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s2); - ensure_equals("third alloc recycles slot 0 again", (S32)s2, 0); - ensure("generation advanced again", c.getCacheGeneration() > gen1); + CHECK((S32)s2 == 0); + CHECK(c.getCacheGeneration() > gen1); - ensure_equals("getNumBitmaps == 1 (slots recycled, not reserved)", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure("slot 0 live after the final alloc", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } // A live active sheet with room keeps absorbing allocations — a // release+recycle cycle doesn't perturb the pen, and no spurious // sheets are created while the active sheet has space. Pins the // active-sheet tracking (mCurrentSheet) staying put across allocs. - template<> template<> - void llfontbitmapcache_gl_object::test<13>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: a recycled active sheet keeps taking allocations while it has room", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -362,7 +363,7 @@ namespace c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s0); c.releaseSheet(EFontGlyphType::Grayscale, s0); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, s1); - ensure_equals("after release+alloc, recycled onto slot 0", (S32)s1, 0); + CHECK((S32)s1 == 0); // Further allocs must keep landing on the recycled sheet (it has // plenty of room — only one 8x8 glyph placed since the rebuild). @@ -370,13 +371,11 @@ namespace { U32 si = 99; c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, si); - ensure_equals("subsequent alloc stays on the recycled sheet", - (S32)si, 0); + UNSCOPED_INFO("a later allocation on the recycled sheet"); + REQUIRE((S32)si == 0); } - ensure_equals("still 1 sheet slot (no extra allocation)", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure("recycled sheet stays live", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + CHECK(c.getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } // Cross-instance global generation: two LLFontBitmapCache instances @@ -384,8 +383,7 @@ namespace // that vertex buffers checking `mLastFontCacheGen` against // getGlobalGeneration() can detect changes that fired on any cache, // not just the one they captured against. - template<> template<> - void llfontbitmapcache_gl_object::test<14>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: two caches' allocations and releases each bump the global generation", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c1; c1.init(2, 2); LLFontBitmapCache c2; c2.init(2, 2); @@ -394,19 +392,23 @@ namespace const S32 g0 = LLFontBitmapCache::getGlobalGeneration(); c1.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, sheet); const S32 g1 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c1 alloc bumped global gen", g1 > g0); + UNSCOPED_INFO("after c1 allocated"); + CHECK(g1 > g0); c2.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, sheet); const S32 g2 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c2 alloc bumped global gen", g2 > g1); + UNSCOPED_INFO("after c2 allocated"); + CHECK(g2 > g1); c1.releaseSheet(EFontGlyphType::Grayscale, 0); const S32 g3 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c1 release bumped global gen", g3 > g2); + UNSCOPED_INFO("after c1 released"); + CHECK(g3 > g2); c2.releaseSheet(EFontGlyphType::Color, 0); const S32 g4 = LLFontBitmapCache::getGlobalGeneration(); - ensure("c2 release bumped global gen", g4 > g3); + UNSCOPED_INFO("after c2 released"); + CHECK(g4 > g3); } // Per-type independence under release: releasing all Color sheets @@ -414,8 +416,7 @@ namespace // queries don't cross-bleed. Pins the per-type vector array layout // — each EFontGlyphType has its own mImageRawVec / pen / released // tracking. - template<> template<> - void llfontbitmapcache_gl_object::test<15>() + TEST_CASE_METHOD(llfontbitmapcache_gl_data, "LLFontBitmapCacheGL: releasing the Color sheet leaves Grayscale alone", "[LLFontBitmapCacheGL]") { LLFontBitmapCache c; c.init(2, 2); @@ -424,48 +425,17 @@ namespace c.nextOpenPos(8, 8, px, py, EFontGlyphType::Grayscale, gs_sheet); c.nextOpenPos(8, 8, px, py, EFontGlyphType::Color, col_sheet); - ensure_equals("Grayscale on its own sheet 0", (S32)gs_sheet, 0); - ensure_equals("Color on its own sheet 0", (S32)col_sheet, 0); + CHECK((S32)gs_sheet == 0); + CHECK((S32)col_sheet == 0); c.releaseSheet(EFontGlyphType::Color, col_sheet); - ensure("Color sheet 0 released", - c.isSheetReleased(EFontGlyphType::Color, 0)); - ensure("Grayscale sheet 0 unaffected by Color release", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + CHECK(c.isSheetReleased(EFontGlyphType::Color, 0)); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); // Allocate again on Grayscale: still lands on sheet 0 (its // trailing slot is live, unaffected by Color side). S32 px2 = 0, py2 = 0; U32 gs_sheet2 = 99; c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, gs_sheet2); - ensure_equals("Grayscale alloc stays on sheet 0 (Color release didn't touch it)", - (S32)gs_sheet2, 0); - } - - // reset() after allocations clears all state — no live sheets, - // pens back at 4, and a subsequent nextOpenPos starts from (4, 4) - // on a fresh sheet. - template<> template<> - void llfontbitmapcache_gl_object::test<11>() - { - LLFontBitmapCache c; - c.init(2, 2); - S32 px = 0, py = 0; U32 sheet = 0; - c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); - c.nextOpenPos(40, 20, px, py, EFontGlyphType::Grayscale, sheet); - ensure_equals("two sheets pending before reset", - c.getNumBitmaps(EFontGlyphType::Grayscale), 1u); - - c.reset(); - ensure_equals("no Grayscale sheets after reset", - c.getNumBitmaps(EFontGlyphType::Grayscale), 0u); - ensure_equals("no Color sheets after reset", - c.getNumBitmaps(EFontGlyphType::Color), 0u); - - // Re-init and verify pens really did reset back to (4,4). - c.init(2, 2); - S32 px2 = -1, py2 = -1; U32 sheet2 = 99; - c.nextOpenPos(8, 8, px2, py2, EFontGlyphType::Grayscale, sheet2); - ensure_equals("post-reset first posX", px2, 4); - ensure_equals("post-reset first posY", py2, 4); + CHECK((S32)gs_sheet2 == 0); } } diff --git a/indra/llrender/tests/llfontbitmapcache_test.cpp b/indra/llrender/tests/llfontbitmapcache_test.cpp index 745a211ca9..796e66b2b1 100644 --- a/indra/llrender/tests/llfontbitmapcache_test.cpp +++ b/indra/llrender/tests/llfontbitmapcache_test.cpp @@ -32,94 +32,67 @@ #include "../llfontbitmapcache.h" -#include "../test/lltut.h" +#include -namespace tut -{ namespace { - // Pure-CPU fixture: no GL context required. The tests in this group - // touch only the bookkeeping state that LLFontBitmapCache maintains - // independently of any GL allocation. - struct llfontbitmapcache_data - { - }; - - typedef test_group llfontbitmapcache_test; - typedef llfontbitmapcache_test::object llfontbitmapcache_object; - tut::llfontbitmapcache_test llfontbitmapcache_testcase("LLFontBitmapCache"); -} - // Construction takes a fresh value from sNextGeneration. Two // back-to-back instances must therefore land on distinct // generations — without per-instance uniqueness, vertex buffers // comparing their stored mLastFontCacheGen across an in-place // font reload would false-match (see llfontbitmapcache.h:139-147). - template<> template<> - void llfontbitmapcache_object::test<1>() + TEST_CASE("LLFontBitmapCache: each new cache takes a fresh and higher generation", "[LLFontBitmapCache]") { LLFontBitmapCache a; LLFontBitmapCache b; - ensure_not_equals("two instances get distinct generations", - a.getCacheGeneration(), b.getCacheGeneration()); - ensure("first generation > 0", a.getCacheGeneration() > 0); - ensure("second generation > first", - b.getCacheGeneration() > a.getCacheGeneration()); + CHECK(a.getCacheGeneration() != b.getCacheGeneration()); + CHECK(a.getCacheGeneration() > 0); + CHECK(b.getCacheGeneration() > a.getCacheGeneration()); } // Static getGlobalGeneration must reflect the last sNextGeneration // bump. Constructing a new cache bumps the counter, so the global // tick must equal the new instance's mGeneration immediately after. - template<> template<> - void llfontbitmapcache_object::test<2>() + TEST_CASE("LLFontBitmapCache: getGlobalGeneration is the newest cache's generation", "[LLFontBitmapCache]") { const S32 before = LLFontBitmapCache::getGlobalGeneration(); LLFontBitmapCache c; const S32 after = LLFontBitmapCache::getGlobalGeneration(); - ensure("global generation strictly increased on construction", - after > before); - ensure_equals("global generation matches new instance's gen", - after, c.getCacheGeneration()); + CHECK(after > before); + CHECK(after == c.getCacheGeneration()); } // init() seeds mBitmapWidth/Height; reset() clears them back to 0 // and bumps the generation counter. Verify both ends. - template<> template<> - void llfontbitmapcache_object::test<3>() + TEST_CASE("LLFontBitmapCache: init sizes the sheets; reset clears them and bumps the generation", "[LLFontBitmapCache]") { LLFontBitmapCache c; const S32 gen_init0 = c.getCacheGeneration(); c.init(/*max_char_width=*/2, /*max_char_height=*/2); // 2*40=80 → next pow2 ≥ 80 → 128, capped at 1024. - ensure_equals("bitmap width set by init", c.getBitmapWidth(), 128); - ensure_equals("bitmap height set by init", c.getBitmapHeight(), 128); + CHECK(c.getBitmapWidth() == 128); + CHECK(c.getBitmapHeight() == 128); c.reset(); - ensure_equals("bitmap width cleared by reset", c.getBitmapWidth(), 0); - ensure_equals("bitmap height cleared by reset", c.getBitmapHeight(), 0); - ensure("reset bumped generation", - c.getCacheGeneration() > gen_init0); + CHECK(c.getBitmapWidth() == 0); + CHECK(c.getBitmapHeight() == 0); + CHECK(c.getCacheGeneration() > gen_init0); } // Out-of-range queries on a freshly-constructed (or reset) cache // must return safe defaults, not crash. EFontGlyphType::Count is // a sentinel — every accessor must reject it. - template<> template<> - void llfontbitmapcache_object::test<4>() + TEST_CASE("LLFontBitmapCache: queries out of range or on an empty cache return safe defaults", "[LLFontBitmapCache]") { LLFontBitmapCache c; - ensure_equals("getNumBitmaps(Count) returns 0", - c.getNumBitmaps(EFontGlyphType::Count), 0u); - ensure("getImageRaw(Count, 0) returns null", - c.getImageRaw(EFontGlyphType::Count, 0) == nullptr); - ensure("getImageGL(Count, 0) returns null", - c.getImageGL(EFontGlyphType::Count, 0) == nullptr); - ensure("getImageRaw on empty Grayscale returns null", - c.getImageRaw(EFontGlyphType::Grayscale, 0) == nullptr); - ensure_equals("getSheetLastUsedTime out-of-range == 0", - c.getSheetLastUsedTime(EFontGlyphType::Grayscale, 0), 0.0); - ensure("isSheetReleased on empty cache reports false (slot doesn't exist)", - !c.isSheetReleased(EFontGlyphType::Grayscale, 0)); + CHECK(c.getNumBitmaps(EFontGlyphType::Count) == 0u); + CHECK(c.getImageRaw(EFontGlyphType::Count, 0) == nullptr); + CHECK(c.getImageGL(EFontGlyphType::Count, 0) == nullptr); + UNSCOPED_INFO("an empty Grayscale vector"); + CHECK(c.getImageRaw(EFontGlyphType::Grayscale, 0) == nullptr); + CHECK(c.getSheetLastUsedTime(EFontGlyphType::Grayscale, 0) == 0.0); + UNSCOPED_INFO("a slot that does not exist is not released"); + CHECK_FALSE(c.isSheetReleased(EFontGlyphType::Grayscale, 0)); } } diff --git a/indra/llrender/tests/llfontfreetype_gl_test.cpp b/indra/llrender/tests/llfontfreetype_gl_test.cpp index 3339f7b510..d6cfddfce0 100644 --- a/indra/llrender/tests/llfontfreetype_gl_test.cpp +++ b/indra/llrender/tests/llfontfreetype_gl_test.cpp @@ -34,12 +34,10 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include -namespace tut -{ namespace { using namespace ll_test; @@ -56,32 +54,26 @@ namespace ll_test::FontStateScope font_scope; }; - typedef test_group llfontfreetype_render_test; - typedef llfontfreetype_render_test::object llfontfreetype_render_object; - tut::llfontfreetype_render_test llfontfreetype_render_testcase("LLFontFreetypeRender"); -} // After getGlyphInfo for an ASCII glyph, the bitmap cache has at // least one Grayscale page, and the returned info has positive // metrics (the FT raster actually ran). - template<> template<> - void llfontfreetype_render_object::test<1>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: getGlyphInfo rasterizes an ASCII glyph onto a Grayscale sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); LLFontGlyphInfo* gi = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("getGlyphInfo returned an entry", gi != nullptr); - ensure("glyph has positive width", gi->mWidth > 0); - ensure("glyph has positive advance", gi->mXAdvance > 0.f); + REQUIRE(gi != nullptr); + CHECK(gi->mWidth > 0); + CHECK(gi->mXAdvance > 0.f); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("cache has at least one Grayscale sheet", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + REQUIRE(cache != nullptr); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); } // collectGarbage is throttled — calling it twice in immediate @@ -91,14 +83,13 @@ namespace // would invalidate sheets if there were anything to evict). // Pins 169390b593 — the cache-sweep gating moved out of the // render hot path. - template<> template<> - void llfontfreetype_render_object::test<2>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: two collectGarbage calls in a row return cleanly", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); // Force at least one glyph onto the atlas so collectGarbage // has something to consider. (void)ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); @@ -107,7 +98,8 @@ namespace // doing real work. Both must complete without crashing. ft->collectGarbage(); ft->collectGarbage(); - ensure("collectGarbage throttled call returned cleanly", true); + UNSCOPED_INFO("both calls returned"); + CHECK(true); } // LLFontManager::collectGarbage drops face cache entries with @@ -116,40 +108,41 @@ namespace // 'A' holds an entry for it, and a face loaded fresh holds none. The // address of the evicted face is no evidence -- the allocator may hand // the same block straight back to the reload. - template<> template<> - void llfontfreetype_render_object::test<3>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: LLFontManager::collectGarbage evicts only faces nothing else holds", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); // A head over the face rasterizes 'A' into the face's glyph cache; // loadFtHead's parameters are makeKey's, so the manager hands the // head and this lookup the same face. LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); (void)ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLPointer face = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face loaded", face.notNull() && face->isValid()); + REQUIRE(face.notNull()); + CHECK(face->isValid()); const U32 glyph_a = face->getCharGlyphIndex(L'A'); - ensure("the head rasterized into this face", - face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); + UNSCOPED_INFO("the head rasterized into this face"); + REQUIRE(face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); // Held by the head and by us: survives a sweep, glyph and all. gFontManagerp->collectGarbage(); - ensure("a held face keeps its glyphs across GC", - face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); - ensure_equals("a held face is still the cached one", - gFontManagerp->getOrCreateFace(makeKey(path)).get(), face.get()); + UNSCOPED_INFO("a held face keeps its glyphs across GC"); + CHECK(face->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) != nullptr); + UNSCOPED_INFO("a held face is still the cached one"); + CHECK(gFontManagerp->getOrCreateFace(makeKey(path)).get() == face.get()); // Held by the cache alone: swept, so the reload is a fresh face. ft = nullptr; face = nullptr; gFontManagerp->collectGarbage(); LLPointer reloaded = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face reloaded", reloaded.notNull() && reloaded->isValid()); - ensure("the reloaded face has no glyphs: the cached one was evicted", - reloaded->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) == nullptr); + REQUIRE(reloaded.notNull()); + CHECK(reloaded->isValid()); + UNSCOPED_INFO("the reloaded face has no glyphs: the cached one was evicted"); + CHECK(reloaded->findGlyphInfo(glyph_a, EFontGlyphType::Grayscale) == nullptr); } // After two getGlyphInfo calls in a row, both glyphs end up in @@ -159,26 +152,24 @@ namespace // font-atlas layer (the disable-on-setSubImage regression would // surface as a crash or as a generation skip on the second // upload). - template<> template<> - void llfontfreetype_render_object::test<4>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: two ASCII glyphs rasterized in a row share one sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'B', EFontGlyphType::Grayscale); - ensure("first glyph allocated", g1 != nullptr); - ensure("second glyph allocated", g2 != nullptr); + REQUIRE(g1 != nullptr); + REQUIRE(g2 != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); + REQUIRE(cache != nullptr); // Both glyphs should land in sheet 0 (a single 1024² sheet // can hold thousands of small ASCII glyphs). - ensure_equals("two ASCII glyphs share sheet 0", - cache->getNumBitmaps(EFontGlyphType::Grayscale), 1u); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); } // Variable-axis weight metric effect: loading InterVariable at @@ -191,36 +182,32 @@ namespace // a >1px advance gap at 32pt 96dpi. Small point sizes can collapse // the diff under FT's grid-fitting; this guards explicitly against // that edge case. - template<> template<> - void llfontfreetype_render_object::test<5>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: InterVariable at weight 900 has a wider 'A' than at weight 300", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); ALFontVarAxes va_light; va_light.wght = 300.f; va_light.wght_set = true; LLPointer ft_light = new LLFontFreetype; - ensure("weight=300 load", - ft_light->loadFace(path, 32.f, 96.f, 96.f, + CHECK(ft_light->loadFace(path, 32.f, 96.f, 96.f, /*is_fallback=*/false, 0, EFontHinting::DEFAULT, /*flags=*/0, va_light)); ALFontVarAxes va_black; va_black.wght = 900.f; va_black.wght_set = true; LLPointer ft_black = new LLFontFreetype; - ensure("weight=900 load", - ft_black->loadFace(path, 32.f, 96.f, 96.f, + CHECK(ft_black->loadFace(path, 32.f, 96.f, 96.f, /*is_fallback=*/false, 0, EFontHinting::DEFAULT, /*flags=*/0, va_black)); const F32 adv_light = ft_light->getXAdvance(L'A'); const F32 adv_black = ft_black->getXAdvance(L'A'); - ensure("light advance positive", adv_light > 0.f); - ensure("black advance positive", adv_black > 0.f); + CHECK(adv_light > 0.f); + CHECK(adv_black > 0.f); // Heavier weight produces wider 'A'. The diff at 32pt 96dpi // between weight=300 and weight=900 is ~1.5-2 px for Inter. - ensure("weight=900 'A' advance > weight=300 'A' advance", - adv_black > adv_light + 0.5f); + CHECK(adv_black > adv_light + 0.5f); } // Sheet re-entry through the rasterizer: rasterize onto sheet 0, @@ -229,23 +216,22 @@ namespace // across eviction cycles) and getCacheGeneration advances so // captured vertex buffers rebuild. Pins slot recycling through the // rasterizer layer (the cache-layer tests pin it at nextOpenPos). - template<> template<> - void llfontfreetype_render_object::test<6>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: a glyph after sheet 0's release reuses the slot and bumps the generation", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("DejaVuSans loaded as head", ft.notNull()); + REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("first glyph allocated", g1 != nullptr); + REQUIRE(g1 != nullptr); // Use getBitmapCache() (non-const) for releaseSheet; the const // overload exposed via getFontBitmapCache() is for queries only. LLFontBitmapCache* cache = ft->getBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure_equals("started with one Grayscale sheet", - cache->getNumBitmaps(EFontGlyphType::Grayscale), 1u); + REQUIRE(cache != nullptr); + UNSCOPED_INFO("before the release"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); const S32 gen_before = cache->getCacheGeneration(); // Honor the purge-before-release contract the production sweep @@ -265,19 +251,16 @@ namespace return false; }); cache->releaseSheet(EFontGlyphType::Grayscale, 0); - ensure("sheet 0 reports released", - cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); + CHECK(cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); // New glyph rebuilds into the recycled slot. Picking a glyph not // already cached avoids hitting the cmap-cache branch. LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'B', EFontGlyphType::Grayscale); - ensure("second glyph allocated post-release", g2 != nullptr); - ensure_equals("released slot recycled (no sheet-vector growth)", - cache->getNumBitmaps(EFontGlyphType::Grayscale), 1u); - ensure("recycled slot is live again", - !cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); - ensure("cache generation advanced after release+alloc", - cache->getCacheGeneration() > gen_before); + REQUIRE(g2 != nullptr); + UNSCOPED_INFO("the released slot was recycled"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == 1u); + CHECK_FALSE(cache->isSheetReleased(EFontGlyphType::Grayscale, 0)); + CHECK(cache->getCacheGeneration() > gen_before); } // Cross-instance global generation contention: rasterizing on @@ -286,25 +269,27 @@ namespace // ticks across faces (llfontbitmapcache.h:81-90) so that vertex // buffers using face B's atlas can detect changes that fired on // any face. - template<> template<> - void llfontfreetype_render_object::test<7>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: rasterizing on each of two faces bumps the global generation", "[LLFontFreetypeRender]") { const std::string a_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string b_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(a_path) || !fileExists(b_path)) - skip("DejaVuSans + InterVariable required"); + SKIP("DejaVuSans + InterVariable required"); LLPointer head_a = loadFtHead(a_path); LLPointer head_b = loadFtHead(b_path); - ensure("both heads loaded", head_a.notNull() && head_b.notNull()); + REQUIRE(head_a.notNull()); + REQUIRE(head_b.notNull()); const S32 gen0 = LLFontBitmapCache::getGlobalGeneration(); (void)head_a->getGlyphInfo(L'A', EFontGlyphType::Grayscale); const S32 gen1 = LLFontBitmapCache::getGlobalGeneration(); - ensure("gen advanced after head_a raster", gen1 > gen0); + UNSCOPED_INFO("after head_a rasterized"); + CHECK(gen1 > gen0); (void)head_b->getGlyphInfo(L'B', EFontGlyphType::Grayscale); const S32 gen2 = LLFontBitmapCache::getGlobalGeneration(); - ensure("gen advanced after head_b raster", gen2 > gen1); + UNSCOPED_INFO("after head_b rasterized"); + CHECK(gen2 > gen1); } // Cross-head sheet eviction: when one freetype's collectGarbage @@ -318,31 +303,33 @@ namespace // "glyphs unload after long idle and never reload"). The fix routes // every lookup through the face's owned cache so all freetypes that // share the face observe the eviction consistently. - template<> template<> - void llfontfreetype_render_object::test<8>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: a sibling head gets a live glyph after the other evicts its sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer head_a = loadFtHead(path); LLPointer head_b = loadFtHead(path); - ensure("both heads loaded", head_a.notNull() && head_b.notNull()); + REQUIRE(head_a.notNull()); + REQUIRE(head_b.notNull()); // Same ALFontFaceKey -> shared ALFontFace via getOrCreateFace. - ensure_equals("siblings share the underlying face wrapper", - head_a->getFontFace(), head_b->getFontFace()); + REQUIRE(head_a->getFontFace() == head_b->getFontFace()); // Cache a glyph through both heads. Face dedup means both // calls return the same entry pointer. LLFontGlyphInfo* a_gi = head_a->getGlyphInfo(L'A', EFontGlyphType::Grayscale); LLFontGlyphInfo* b_gi = head_b->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("head_a got glyph", a_gi != nullptr); - ensure("head_b got glyph", b_gi != nullptr); - ensure_equals("shared face returns the same glyph entry", a_gi, b_gi); + REQUIRE(a_gi != nullptr); + REQUIRE(b_gi != nullptr); + REQUIRE(a_gi == b_gi); const auto pre_entry = a_gi->mPhaseSlots[0].mBitmapEntry; - ensure("phase 0 bitmap entry references a real sheet", - pre_entry.first == EFontGlyphType::Grayscale && pre_entry.second >= 0); + { + INFO("phase 0 references a real sheet"); + CHECK(pre_entry.first == EFontGlyphType::Grayscale); + CHECK(pre_entry.second >= 0); + } // Simulate head_a's collectGarbage path on the shared face: // delete face-owned entries that reference this sheet, then @@ -351,7 +338,7 @@ namespace // pointer; post-fix there is no per-head cache to leak. const auto target = pre_entry; LLFontBitmapCache* cache = head_a->getBitmapCache(); - ensure("bitmap cache present", cache != nullptr); + REQUIRE(cache != nullptr); head_a->getFontFace()->erase_glyph_entries( [target](const LLFontGlyphInfo* gi) { @@ -364,19 +351,16 @@ namespace return false; }); cache->releaseSheet(target.first, static_cast(target.second)); - ensure("target sheet reports released", - cache->isSheetReleased(target.first, static_cast(target.second))); + CHECK(cache->isSheetReleased(target.first, static_cast(target.second))); // head_b looks up the same glyph again. The lookup must: // 1. not crash on a dangling pointer, // 2. produce a non-null entry, // 3. land on a live sheet (not the just-released one). LLFontGlyphInfo* b_gi2 = head_b->getGlyphInfo(L'A', EFontGlyphType::Grayscale); - ensure("head_b got fresh glyph after sibling-driven eviction", - b_gi2 != nullptr); + REQUIRE(b_gi2 != nullptr); const auto post_entry = b_gi2->mPhaseSlots[0].mBitmapEntry; - ensure("post-eviction phase 0 references a live sheet", - !cache->isSheetReleased(post_entry.first, + CHECK_FALSE(cache->isSheetReleased(post_entry.first, static_cast(post_entry.second))); } @@ -386,21 +370,23 @@ namespace // on a live sheet. (The pre-fix bug bit only the cross-head // scenario, but this test pins the single-head invariant so future // refactors of the eviction path don't silently regress.) - template<> template<> - void llfontfreetype_render_object::test<9>() + TEST_CASE_METHOD(llfontfreetype_render_data, "LLFontFreetypeRender: a glyph evicted with its sheet comes back on a live sheet", "[LLFontFreetypeRender]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer ft = loadFtHead(path); - ensure("ft loaded", ft.notNull()); + REQUIRE(ft.notNull()); LLFontGlyphInfo* g1 = ft->getGlyphInfo(L'Z', EFontGlyphType::Grayscale); - ensure("got glyph", g1 != nullptr); + REQUIRE(g1 != nullptr); const auto target = g1->mPhaseSlots[0].mBitmapEntry; - ensure("bitmap entry valid", - target.first == EFontGlyphType::Grayscale && target.second >= 0); + { + INFO("phase 0 references a real sheet"); + CHECK(target.first == EFontGlyphType::Grayscale); + CHECK(target.second >= 0); + } // Run the same delete + release dance the GC path uses. ft->getFontFace()->erase_glyph_entries( @@ -421,10 +407,9 @@ namespace // (now empty for this glyph), falls through to addShapedGlyphFromFont, // and rasterizes onto a live sheet. LLFontGlyphInfo* g2 = ft->getGlyphInfo(L'Z', EFontGlyphType::Grayscale); - ensure("re-requested glyph allocated post-eviction", g2 != nullptr); + REQUIRE(g2 != nullptr); const auto post = g2->mPhaseSlots[0].mBitmapEntry; - ensure("post-eviction bitmap entry references a live sheet", - !ft->getBitmapCache()->isSheetReleased( + CHECK_FALSE(ft->getBitmapCache()->isSheetReleased( post.first, static_cast(post.second))); } } diff --git a/indra/llrender/tests/llfontfreetype_test.cpp b/indra/llrender/tests/llfontfreetype_test.cpp index 6903243a8f..d5efb10982 100644 --- a/indra/llrender/tests/llfontfreetype_test.cpp +++ b/indra/llrender/tests/llfontfreetype_test.cpp @@ -38,12 +38,10 @@ #include "llfonttest_helpers.h" -#include "../test/lltut.h" +#include #include -namespace tut -{ namespace { using namespace ll_test; @@ -57,402 +55,98 @@ namespace ~llfontfreetype_data() { LLFontManager::cleanupClass(); } }; - typedef test_group llfontfreetype_test; - typedef llfontfreetype_test::object llfontfreetype_object; - tut::llfontfreetype_test llfontfreetype_testcase("LLFontFreetype"); -} - - // initClass is idempotent — repeat calls don't replace gFontManagerp. - template<> template<> - void llfontfreetype_object::test<1>() - { - ensure("gFontManagerp non-null after init", gFontManagerp != nullptr); - LLFontManager* before = gFontManagerp; - LLFontManager::initClass(); // second call: no-op - ensure_equals("gFontManagerp unchanged on repeat initClass", - gFontManagerp, before); - } - - // getOrCreateFace returns the same face for equal keys; differing - // point_size produces a distinct face. Pins the cache identity - // contract that LLFontFreetype's mFace sharing relies on. - template<> template<> - void llfontfreetype_object::test<2>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - - LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); - LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); - LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path, 18.f)); - ensure("face a loaded", a.notNull() && a->isValid()); - ensure("face b loaded", b.notNull() && b->isValid()); - ensure("face c loaded", c.notNull() && c->isValid()); - ensure_equals("equal keys -> same face pointer", - a.get(), b.get()); - ensure_not_equals("different point_size -> distinct face", - a.get(), c.get()); - } - - // loadFace on a real file returns true and populates getName. - template<> template<> - void llfontfreetype_object::test<3>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - LLPointer ft = loadFt(path); - ensure("loadFace succeeded", ft.notNull()); - ensure_equals("getName matches filename", ft->getName(), path); - } - - // loadFace on a missing file returns false; callers that own the - // LLFontFreetype must see a safe-to-destruct instance. - template<> template<> - void llfontfreetype_object::test<4>() - { - LLPointer ft = new LLFontFreetype; - const bool ok = ft->loadFace("does/not/exist.ttf", - 14.f, 96.f, 96.f, true, 0, - EFontHinting::DEFAULT, 0); - ensure("loadFace returns false on missing file", !ok); - // Don't deref ft beyond this — destructor must run cleanly. - ft = nullptr; - ensure("destruct after failed load did not crash", true); - } - - // getCharGlyphIndex returns non-zero for ASCII present in the cmap - // and zero for codepoints absent from the cmap. Second call hits - // the cache (no observable difference, but exercises the path). - template<> template<> - void llfontfreetype_object::test<5>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - LLPointer ft = loadFt(path); - ensure("DejaVuSans loaded", ft.notNull()); - const U32 a1 = ft->getCharGlyphIndex(L'A'); - const U32 a2 = ft->getCharGlyphIndex(L'A'); - ensure_not_equals("'A' has a non-zero glyph index", a1, 0u); - ensure_equals("repeated lookup returns same value (cache hit)", - a1, a2); - // U+E000 is in the BMP private-use area — DejaVuSans doesn't - // populate it; getCharGlyphIndex must return 0 (cached miss - // is meaningful — it lets the renderer fall through to - // fallbacks without re-walking the cmap). - ensure_equals("PUA codepoint has glyph index 0", - ft->getCharGlyphIndex(0xE000), 0u); - } - - // faceHasGlyph mirrors getCharGlyphIndex's notion of "covers this - // codepoint." Verify positive (ASCII present) and negative - // (Noto-COLRv1 lacks U+FE0F) cases — pins the VS-16 strip - // discriminator used by shape_sub_run. - template<> template<> - void llfontfreetype_object::test<6>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(dejavu) || !fileExists(noto)) - skip("DejaVuSans.woff2 + Noto-COLRv1.ttf both required"); - - LLPointer dv = loadFt(dejavu); - LLPointer nt = loadFt(noto); - ensure("both loaded", dv.notNull() && nt.notNull()); - ensure("DejaVuSans covers 'a'", dv->faceHasGlyph(L'a')); - ensure("Noto-COLRv1 lacks U+FE0F", - !nt->faceHasGlyph(0xFE0F)); - ensure("Noto-COLRv1 covers ZWJ (U+200D)", - nt->faceHasGlyph(0x200D)); - } - - // useSubpixelPen depends on hinting and color/svg flags: - // FORCE_AUTOHINT on a non-color font => true; DEFAULT hinting => false; - // a color font (regardless of hinting) => false. Pins the rule at - // alfontface.cpp:99. - template<> template<> - void llfontfreetype_object::test<7>() - { - const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); - - LLPointer a = loadFt(path, /*is_fallback=*/true, - 14.f, -1, EFontHinting::DEFAULT); - LLPointer b = loadFt(path, /*is_fallback=*/true, - 14.f, -1, EFontHinting::FORCE_AUTOHINT); - ensure("both loaded", a.notNull() && b.notNull()); - ensure("DEFAULT hinting -> useSubpixelPen=false", - !a->useSubpixelPen()); - ensure("FORCE_AUTOHINT on non-color font -> useSubpixelPen=true", - b->useSubpixelPen()); - - // A color font (Noto-COLRv1) with FORCE_AUTOHINT still reports - // false because the rule short-circuits on color/svg. - const std::string colr = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (fileExists(colr)) - { - LLPointer c = loadFt(colr, true, 14.f, -1, - EFontHinting::FORCE_AUTOHINT); - ensure("Noto-COLRv1 loaded", c.notNull()); - ensure("color font -> useSubpixelPen=false even with FORCE_AUTOHINT", - !c->useSubpixelPen()); - } - } - - // isFixedWidth: DejaVuSansMono yes, DejaVuSans no. - // getAllowMonospaceLigatures defaults false; setter toggles. - template<> template<> - void llfontfreetype_object::test<8>() - { - const std::string mono = std::string(kFontDir) + "DejaVuSansMono.woff2"; - const std::string prop = std::string(kFontDir) + "DejaVuSans.woff2"; - if (!fileExists(mono) || !fileExists(prop)) - skip("DejaVuSans + DejaVuSansMono required"); - - LLPointer m = loadFt(mono); - LLPointer p = loadFt(prop); - ensure("Mono is fixed-width", m->isFixedWidth()); - ensure("Prop is not fixed-width", !p->isFixedWidth()); - ensure("ligatures default off on Mono", - !m->getAllowMonospaceLigatures()); - m->setAllowMonospaceLigatures(true); - ensure("ligatures-on toggle takes effect", - m->getAllowMonospaceLigatures()); - m->setAllowMonospaceLigatures(false); - ensure("ligatures-off toggle takes effect", - !m->getAllowMonospaceLigatures()); - } - - // getStyle reflects the FT face's intrinsic flags. DejaVuSans is - // NORMAL; DejaVuSans-Bold has the BOLD bit set after load. - template<> template<> - void llfontfreetype_object::test<9>() - { - const std::string normal = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string bold = std::string(kFontDir) + "DejaVuSans-Bold.woff2"; - if (!fileExists(normal) || !fileExists(bold)) - skip("DejaVuSans + DejaVuSans-Bold required"); - - LLPointer n = loadFt(normal); - LLPointer b = loadFt(bold); - ensure("normal style has no BOLD bit", - (n->getStyle() & LLFontGL::BOLD) == 0); - ensure("bold file sets BOLD bit", - (b->getStyle() & LLFontGL::BOLD) != 0); - } - - // selectShapingFace walks the fallback chain by codepoint coverage: - // ASCII routes to the head, CJK routes to a CJK fallback, emoji - // routes to the emoji fallback. Pins multi-hop priority resolution - // at llfontfreetype.cpp:266-284. - template<> template<> - void llfontfreetype_object::test<10>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string han = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; - const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(dejavu) || !fileExists(han) || !fileExists(emoji)) - skip("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); - - LLPointer head = loadFt(dejavu); - LLPointer cjk = loadFt(han); - LLPointer emo = loadFt(emoji); - ensure("all three loaded", - head.notNull() && cjk.notNull() && emo.notNull()); - head->addFallbackFont(cjk); - head->addFallbackFont(emo); - - U32 idx = 0; - // ASCII 'a' lives in the head's cmap → head wins. - ensure_equals("'a' resolves to head", - head->selectShapingFace(L'a', idx), head.get()); - ensure_not_equals("head produced a non-zero glyph for 'a'", idx, 0u); - - // U+4F60 你 lives in SourceHanSans, not in DejaVuSans → CJK fallback. - idx = 0; - ensure_equals("CJK codepoint resolves to SourceHanSans fallback", - head->selectShapingFace(0x4F60, idx), cjk.get()); - - // U+1F525 (fire emoji) lives in Noto-COLRv1, not in either text - // face → emoji fallback wins. - idx = 0; - ensure_equals("emoji codepoint resolves to Noto-COLRv1 fallback", - head->selectShapingFace(0x1F525, idx), emo.get()); - } - - // mShapingFaceResolution caches selectShapingFace decisions; adding - // a new fallback must invalidate that cache so a previously-resolved - // codepoint can re-route through the newly-attached chain. Pins the - // cache-clear on addFallbackFont at llfontfreetype.cpp:434. - template<> template<> - void llfontfreetype_object::test<11>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; - if (!fileExists(dejavu) || !fileExists(emoji)) - skip("DejaVuSans + Noto-COLRv1 required"); - - LLPointer head = loadFt(dejavu); - LLPointer emo = loadFt(emoji); - ensure("both loaded", head.notNull() && emo.notNull()); - - // First lookup, no fallback registered. 'a' resolves to head; - // emoji resolves to head with idx=0 (no fallback covers it). - U32 idx_a1 = 0, idx_e1 = 0; - const LLFontFreetype* a1 = head->selectShapingFace(L'a', idx_a1); - const LLFontFreetype* e1 = head->selectShapingFace(0x1F525, idx_e1); - ensure_equals("'a' first resolution -> head", a1, head.get()); - ensure_equals("emoji first resolution -> head (no fallback)", - e1, head.get()); - ensure_equals("emoji on plain head produced glyph 0", - idx_e1, 0u); - - // Second lookup hits the cache and returns the same pointer. - U32 idx_a2 = 0; - ensure_equals("repeat 'a' lookup hits cache", - head->selectShapingFace(L'a', idx_a2), head.get()); - - // Add the emoji fallback. The cache must invalidate so the next - // emoji lookup re-runs the walker through the new chain. - head->addFallbackFont(emo); - - U32 idx_e2 = 0; - ensure_equals("after addFallbackFont, emoji re-resolves to fallback", - head->selectShapingFace(0x1F525, idx_e2), emo.get()); - } - - // Functor gating: a fallback whose char_functor returns false for - // codepoint X is excluded from X's resolution, even if its cmap - // covers X. Pins the must-accept gate at llfontfreetype.cpp:268-273. - template<> template<> - void llfontfreetype_object::test<12>() - { - const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; - const std::string emoji = std::string(kFontDir) + "TwemojiSVG.woff2"; - if (!fileExists(dejavu) || !fileExists(emoji)) - skip("DejaVuSans + TwemojiSVG required"); - - LLPointer head = loadFt(dejavu); - LLPointer emo = loadFt(emoji); - ensure("both loaded", head.notNull() && emo.notNull()); - - // Functor accepts only emoji-range codepoints; rejects ASCII. - // If TwemojiSVG happens to carry ASCII outlines, the functor - // forces the head to win 'a' regardless. - auto pictograph_only = [](llwchar c) -> bool { return c >= 0x1F000; }; - head->addFallbackFont(emo, pictograph_only); - - U32 idx = 0; - ensure_equals("'a' resolves to head despite Twemoji ASCII coverage", - head->selectShapingFace(L'a', idx), head.get()); - // Functor allows emoji range, so emoji codepoints route to Twemoji. - idx = 0; - ensure_equals("emoji codepoint passes functor and resolves to Twemoji", - head->selectShapingFace(0x1F525, idx), emo.get()); - } // ------------------------------------------------------------- // ALFontFace group: identity key, COLR/SVG/color/wght probes, // cmap cache, refcount sharing semantics. // ------------------------------------------------------------- -namespace -{ struct alfontface_data { alfontface_data() { LLFontManager::initClass(); } ~alfontface_data() { LLFontManager::cleanupClass(); } }; - typedef test_group alfontface_test; - typedef alfontface_test::object alfontface_object; - tut::alfontface_test alfontface_testcase("ALFontFace"); -} // ALFontFaceKey equality + hash: equal keys equal+hash-equal; // any single-field difference produces unequal keys. - template<> template<> - void alfontface_object::test<1>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: ALFontFaceKey copies are equal and hash alike; one changed field is not", "[ALFontFace]") { ALFontFaceKey a{ "x.ttf", 0, 14.f, 96.f, 96.f, EFontHinting::DEFAULT, 0 }; ALFontFaceKey b = a; - ensure("identical keys equal", - a == b); - ensure_equals("identical keys hash equal", - hash_value(a), hash_value(b)); + CHECK(a == b); + CHECK(hash_value(a) == hash_value(b)); b.point_size = 18.f; - ensure("differing point_size keys not equal", !(a == b)); + UNSCOPED_INFO("point_size differs"); + CHECK_FALSE(a == b); b = a; b.face_index = 1; - ensure("differing face_index keys not equal", !(a == b)); + UNSCOPED_INFO("face_index differs"); + CHECK_FALSE(a == b); b = a; b.var_axes.wght = 600.f; b.var_axes.wght_set = true; - ensure("differing wght keys not equal", !(a == b)); + UNSCOPED_INFO("wght differs"); + CHECK_FALSE(a == b); b = a; b.hinting = EFontHinting::FORCE_AUTOHINT; - ensure("differing hinting keys not equal", !(a == b)); + UNSCOPED_INFO("hinting differs"); + CHECK_FALSE(a == b); b = a; b.flags = LLFontGL::BOLD; - ensure("differing flags keys not equal", !(a == b)); + UNSCOPED_INFO("flags differ"); + CHECK_FALSE(a == b); } - // hasColrV1: Noto-COLRv1 yes, DejaVuSans no. - template<> template<> - void alfontface_object::test<2>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: hasColrV1 is true for Noto-COLRv1 and false for DejaVuSans", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dv = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(noto) || !fileExists(dv)) - skip("Noto-COLRv1 + DejaVuSans required"); + SKIP("Noto-COLRv1 + DejaVuSans required"); LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - ensure("both faces valid", nf->isValid() && df->isValid()); - ensure("Noto-COLRv1 hasColrV1", nf->hasColrV1()); - ensure("DejaVuSans !hasColrV1", !df->hasColrV1()); + CHECK(nf->isValid()); + CHECK(df->isValid()); + CHECK(nf->hasColrV1()); + CHECK_FALSE(df->hasColrV1()); } // hasColor: Noto-COLRv1 yes (FT_HAS_COLOR is set for any color // table); DejaVuSans no. - template<> template<> - void alfontface_object::test<3>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: hasColor is true for Noto-COLRv1 and false for DejaVuSans", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dv = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(noto) || !fileExists(dv)) - skip("Noto-COLRv1 + DejaVuSans required"); + SKIP("Noto-COLRv1 + DejaVuSans required"); LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - ensure("Noto-COLRv1 hasColor", nf->hasColor()); - ensure("DejaVuSans !hasColor", !df->hasColor()); + CHECK(nf->hasColor()); + CHECK_FALSE(df->hasColor()); } // hasSvg: Noto-COLRv1 doesn't ship OT-SVG; DejaVuSans doesn't // either. Both should report false. (TwemojiSVG would be the // positive case but isn't checked into the fixture set.) - template<> template<> - void alfontface_object::test<4>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: hasSvg is false for both Noto-COLRv1 and DejaVuSans", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dv = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(noto) || !fileExists(dv)) - skip("Noto-COLRv1 + DejaVuSans required"); + SKIP("Noto-COLRv1 + DejaVuSans required"); LLPointer nf = gFontManagerp->getOrCreateFace(makeKey(noto)); LLPointer df = gFontManagerp->getOrCreateFace(makeKey(dv)); - ensure("Noto-COLRv1 !hasSvg", !nf->hasSvg()); - ensure("DejaVuSans !hasSvg", !df->hasSvg()); + CHECK_FALSE(nf->hasSvg()); + CHECK_FALSE(df->hasSvg()); } // wghtAxisSet: InterVariable is variable; loading at weight=600 // sets the wght axis. Loading at weight=-1 leaves wght axis off. - template<> template<> - void alfontface_object::test<5>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: wghtAxisSet is set by a weight on InterVariable and not without one", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); ALFontFaceKey k_default = makeKey(path); // wght not set -> face takes the file's default @@ -462,98 +156,92 @@ namespace LLPointer def = gFontManagerp->getOrCreateFace(k_default); LLPointer bld = gFontManagerp->getOrCreateFace(k_bold); - ensure("both loaded", def->isValid() && bld->isValid()); - ensure("wght unset -> wghtAxisSet=false", !def->wghtAxisSet()); - ensure("wght=600 on variable face -> wghtAxisSet=true", - bld->wghtAxisSet()); + CHECK(def->isValid()); + CHECK(bld->isValid()); + CHECK_FALSE(def->wghtAxisSet()); + CHECK(bld->wghtAxisSet()); } // isFixedWidth (face-level): DejaVuSansMono yes, DejaVuSans no. // Mirrors the LLFontFreetype-level test but exercises the // wrapper's mIsFixedWidth field directly. - template<> template<> - void alfontface_object::test<6>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: isFixedWidth is true for DejaVuSansMono and false for DejaVuSans", "[ALFontFace]") { const std::string mono = std::string(kFontDir) + "DejaVuSansMono.woff2"; const std::string prop = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(mono) || !fileExists(prop)) - skip("DejaVuSans + DejaVuSansMono required"); + SKIP("DejaVuSans + DejaVuSansMono required"); LLPointer m = gFontManagerp->getOrCreateFace(makeKey(mono)); LLPointer p = gFontManagerp->getOrCreateFace(makeKey(prop)); - ensure("Mono isFixedWidth", m->isFixedWidth()); - ensure("Prop !isFixedWidth", !p->isFixedWidth()); + CHECK(m->isFixedWidth()); + CHECK_FALSE(p->isFixedWidth()); } // getCharGlyphIndex matches FT_Get_Char_Index for present glyphs; // returns 0 for absent ones. Also the caching path: a 0 result // is just as cacheable as a non-zero result. - template<> template<> - void alfontface_object::test<7>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: getCharGlyphIndex is stable for a present glyph and 0 for an absent one", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer f = gFontManagerp->getOrCreateFace(makeKey(path)); const U32 g_a = f->getCharGlyphIndex(L'A'); const U32 g_a_again = f->getCharGlyphIndex(L'A'); - ensure_not_equals("'A' has a glyph", g_a, 0u); - ensure_equals("repeat 'A' lookup hits cache (same value)", - g_a, g_a_again); - ensure_equals("absent codepoint -> 0", - f->getCharGlyphIndex(0xE000), 0u); + CHECK(g_a != 0u); + UNSCOPED_INFO("a repeat lookup hits the cache"); + CHECK(g_a == g_a_again); + UNSCOPED_INFO("an absent codepoint"); + CHECK(f->getCharGlyphIndex(0xE000) == 0u); // Cache the miss too — second call should also be 0. - ensure_equals("absent codepoint cached miss is also 0", - f->getCharGlyphIndex(0xE000), 0u); + UNSCOPED_INFO("the cached miss"); + CHECK(f->getCharGlyphIndex(0xE000) == 0u); } // Refcount sharing: two LLFontFreetype instances that point at // the same ALFontFace via getOrCreateFace must keep the face // alive while either lives. The face cache also holds a ref, // so getNumRefs >= 3 (cache + a + b) when both shares exist. - template<> template<> - void alfontface_object::test<8>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: two lookups share a face and the cache keeps it after both let go", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path)); LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure_equals("a == b (shared face)", a.get(), b.get()); - ensure("refcount >= 3 (cache + a + b)", - a->getNumRefs() >= 3); + REQUIRE(a.get() == b.get()); + UNSCOPED_INFO("refs held by the cache and a and b"); + CHECK(a->getNumRefs() >= 3); ALFontFace* raw = a.get(); b = nullptr; // drop b's ref - ensure("face still alive after b dropped", - raw->getNumRefs() >= 2); + UNSCOPED_INFO("face still alive after b dropped"); + CHECK(raw->getNumRefs() >= 2); a = nullptr; // drop a's ref // Cache still holds a ref, so the face is alive but has // refcount 1. Verify by re-fetching: should return the same // pointer (cache hit), not a freshly-loaded face. LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure_equals("cache kept face alive after both refs dropped", - c.get(), raw); + UNSCOPED_INFO("cache kept face alive after both refs dropped"); + CHECK(c.get() == raw); } // resetBitmapCache clears atlas state without destroying the face. // A subsequent getCharGlyphIndex still works (cmap survives). - template<> template<> - void alfontface_object::test<9>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: resetBitmapCache keeps the face valid with its cmap and its atlas", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); LLPointer f = gFontManagerp->getOrCreateFace(makeKey(path)); - ensure("face valid before reset", f->isValid()); + CHECK(f->isValid()); f->resetBitmapCache(); - ensure("face still valid after resetBitmapCache", - f->isValid()); - ensure("cmap survives reset", - f->getCharGlyphIndex(L'A') != 0u); - ensure("bitmap cache pointer still present", - f->getBitmapCache() != nullptr); + CHECK(f->isValid()); + UNSCOPED_INFO("cmap survives reset"); + CHECK(f->getCharGlyphIndex(L'A') != 0u); + CHECK(f->getBitmapCache() != nullptr); } // paletteIndex() is computed at load time from LLFontGL::sUseDarkEmojiPalette @@ -562,12 +250,11 @@ namespace // FT_PALETTE_FOR_DARK_BACKGROUND palette the second load still returns 0 // and we skip the equality check rather than fail. Pins b6ba746728's // load-time read at alfontface.cpp:122-137. - template<> template<> - void alfontface_object::test<10>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: paletteIndex follows sUseDarkEmojiPalette as it was at load", "[ALFontFace]") { const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; if (!fileExists(noto)) - skip("Noto-COLRv1.ttf not present"); + SKIP("Noto-COLRv1.ttf not present"); const bool saved_flag = LLFontGL::sUseDarkEmojiPalette; @@ -578,10 +265,14 @@ namespace U32 pi_default; { LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face valid (dark=false)", f.notNull() && f->isValid()); + { + INFO("face valid (dark=false)"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + } pi_default = f->paletteIndex(); - ensure_equals("paletteIndex defaults to 0 with dark=false", - pi_default, 0u); + UNSCOPED_INFO("loaded with dark=false"); + CHECK(pi_default == 0u); } // Drop our ref; collectGarbage evicts since cache holds the only ref. gFontManagerp->collectGarbage(); @@ -590,7 +281,11 @@ namespace U32 pi_dark; { LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face valid (dark=true)", f.notNull() && f->isValid()); + { + INFO("face valid (dark=true)"); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + } pi_dark = f->paletteIndex(); } @@ -602,42 +297,39 @@ namespace // does, pi_dark is non-zero (the index of that palette); if it // doesn't, pi_dark stays 0 and we skip the assertion. if (pi_dark == 0) - skip("font has no FT_PALETTE_FOR_DARK_BACKGROUND palette"); + SKIP("font has no FT_PALETTE_FOR_DARK_BACKGROUND palette"); - ensure_not_equals("dark=true selected a non-zero palette index", - pi_dark, 0u); + CHECK(pi_dark != 0u); } // Variable axis on a non-variable font: requesting weight=600 on // DejaVuSans (which lacks a wght axis) leaves wghtAxisSet=false — // setVariationAxis returned false and the load fell through. Pins // the negative branch at alfontface.cpp:139-146. - template<> template<> - void alfontface_object::test<11>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: a weight on a face with no wght axis leaves wghtAxisSet false", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); ALFontFaceKey k = makeKey(path); k.var_axes.wght = 600.f; k.var_axes.wght_set = true; LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face loaded", f.notNull() && f->isValid()); - ensure("non-variable face leaves wghtAxisSet=false", - !f->wghtAxisSet()); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + CHECK_FALSE(f->wghtAxisSet()); } // FaceKey weight is part of the identity key — distinct weight // values map to distinct face cache entries even on the same file. // Existing test 5 confirms the wghtAxisSet flag side; this pins // the cache-identity side at alfontface.h:64-76. - template<> template<> - void alfontface_object::test<12>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: distinct weights make distinct keys and distinct cached faces", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(path)) - skip("InterVariable.woff2 not present"); + SKIP("InterVariable.woff2 not present"); ALFontFaceKey k_400 = makeKey(path); k_400.var_axes.wght = 400.f; @@ -646,15 +338,14 @@ namespace k_600.var_axes.wght = 600.f; k_600.var_axes.wght_set = true; - ensure("k_400 != k_600", !(k_400 == k_600)); - ensure_not_equals("hash differs by weight", - hash_value(k_400), hash_value(k_600)); + CHECK_FALSE(k_400 == k_600); + CHECK(hash_value(k_400) != hash_value(k_600)); LLPointer f400 = gFontManagerp->getOrCreateFace(k_400); LLPointer f600 = gFontManagerp->getOrCreateFace(k_600); - ensure("both faces valid", f400->isValid() && f600->isValid()); - ensure_not_equals("distinct weights produce distinct face entries", - f400.get(), f600.get()); + CHECK(f400->isValid()); + CHECK(f600->isValid()); + CHECK(f400.get() != f600.get()); } // ital/wdth/slnt axis plumbing: distinct ALFontVarAxes values must @@ -663,12 +354,11 @@ namespace // to the key. The bundled fonts don't actually expose ital/wdth/ // slnt axes (so setVariationAxis silently no-ops), but the key // identity check fires regardless of whether the axis takes effect. - template<> template<> - void alfontface_object::test<13>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: ital and wdth and slnt values are part of the key and its hash", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); ALFontFaceKey k_default = makeKey(path); ALFontFaceKey k_ital = makeKey(path); @@ -681,32 +371,29 @@ namespace k_slnt.var_axes.slnt = -12.f; k_slnt.var_axes.slnt_set = true; - ensure("default != ital", !(k_default == k_ital)); - ensure("default != wdth", !(k_default == k_wdth)); - ensure("default != slnt", !(k_default == k_slnt)); - ensure("ital != wdth", !(k_ital == k_wdth)); - ensure("wdth != slnt", !(k_wdth == k_slnt)); - ensure_not_equals("default hash differs from ital", - hash_value(k_default), hash_value(k_ital)); - ensure_not_equals("default hash differs from wdth", - hash_value(k_default), hash_value(k_wdth)); - ensure_not_equals("default hash differs from slnt", - hash_value(k_default), hash_value(k_slnt)); + CHECK_FALSE(k_default == k_ital); + CHECK_FALSE(k_default == k_wdth); + CHECK_FALSE(k_default == k_slnt); + CHECK_FALSE(k_ital == k_wdth); + CHECK_FALSE(k_wdth == k_slnt); + CHECK(hash_value(k_default) != hash_value(k_ital)); + CHECK(hash_value(k_default) != hash_value(k_wdth)); + CHECK(hash_value(k_default) != hash_value(k_slnt)); // Identical axis values produce equal keys + equal hashes. ALFontFaceKey k_ital_dup = makeKey(path); k_ital_dup.var_axes.ital = 1.f; k_ital_dup.var_axes.ital_set = true; - ensure("ital == ital_dup", k_ital == k_ital_dup); - ensure_equals("ital hash matches dup", - hash_value(k_ital), hash_value(k_ital_dup)); + CHECK(k_ital == k_ital_dup); + CHECK(hash_value(k_ital) == hash_value(k_ital_dup)); // Cache identity round-trips through the face manager. LLPointer fd = gFontManagerp->getOrCreateFace(k_default); LLPointer fi = gFontManagerp->getOrCreateFace(k_ital); - ensure("both faces valid", fd->isValid() && fi->isValid()); - ensure_not_equals("distinct ital settings produce distinct face entries", - fd.get(), fi.get()); + CHECK(fd->isValid()); + CHECK(fi->isValid()); + UNSCOPED_INFO("distinct ital settings are distinct cached faces"); + CHECK(fd.get() != fi.get()); } // Faces that don't expose the requested axis leave the matching @@ -714,12 +401,11 @@ namespace // don't carry ital/wdth/slnt; verify the silent-no-op contract // and that requesting an unsupported axis doesn't poison the // existing wghtAxisSet path. - template<> template<> - void alfontface_object::test<14>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: axes a face lacks leave its ital and wdth and slnt flags unset", "[ALFontFace]") { const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(path)) - skip("DejaVuSans.woff2 not present"); + SKIP("DejaVuSans.woff2 not present"); ALFontFaceKey k = makeKey(path); k.var_axes.ital = 1.f; @@ -730,38 +416,313 @@ namespace k.var_axes.slnt_set = true; LLPointer f = gFontManagerp->getOrCreateFace(k); - ensure("face loaded", f.notNull() && f->isValid()); - ensure("DejaVu lacks ital axis -> italAxisSet false", - !f->italAxisSet()); - ensure("DejaVu lacks wdth axis -> wdthAxisSet false", - !f->wdthAxisSet()); - ensure("DejaVu lacks slnt axis -> slntAxisSet false", - !f->slntAxisSet()); + REQUIRE(f.notNull()); + CHECK(f->isValid()); + CHECK_FALSE(f->italAxisSet()); + CHECK_FALSE(f->wdthAxisSet()); + CHECK_FALSE(f->slntAxisSet()); } // ALFontVarAxes equality: the comparison ignores value fields when // *_set is false (an unset axis MUST equal another unset axis with // a different stale value). Required so cache lookups for "no axes // set" don't depend on uninitialised float bits. - template<> template<> - void alfontface_object::test<15>() + TEST_CASE_METHOD(alfontface_data, "ALFontFace: ALFontVarAxes equality ignores the values of axes that are not set", "[ALFontFace]") { ALFontVarAxes a; ALFontVarAxes b; - ensure("default ctor equal", a == b); + CHECK(a == b); // Differing values with *_set=false still equal. b.ital = 0.5f; b.wdth = 99.f; b.slnt = -3.f; - ensure("unset values are ignored in equality", a == b); + UNSCOPED_INFO("unset values are ignored"); + CHECK(a == b); // Once a set flag flips, value differences matter. a.ital_set = true; a.ital = 1.f; b.ital_set = true; b.ital = 0.5f; - ensure("set ital with different values is not equal", - !(a == b)); + UNSCOPED_INFO("ital set on both with different values"); + CHECK_FALSE(a == b); b.ital = 1.f; - ensure("set ital with matching values is equal", a == b); + UNSCOPED_INFO("ital set on both with the same value"); + CHECK(a == b); + } + + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: initClass is idempotent and keeps the same gFontManagerp", "[LLFontFreetype]") + { + REQUIRE(gFontManagerp != nullptr); + LLFontManager* before = gFontManagerp; + LLFontManager::initClass(); // second call: no-op + CHECK(gFontManagerp == before); + } + + // getOrCreateFace returns the same face for equal keys; differing + // point_size produces a distinct face. Pins the cache identity + // contract that LLFontFreetype's mFace sharing relies on. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: getOrCreateFace shares a face for equal keys but not across point sizes", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + + LLPointer a = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); + LLPointer b = gFontManagerp->getOrCreateFace(makeKey(path, 14.f)); + LLPointer c = gFontManagerp->getOrCreateFace(makeKey(path, 18.f)); + REQUIRE(a.notNull()); + CHECK(a->isValid()); + REQUIRE(b.notNull()); + CHECK(b->isValid()); + REQUIRE(c.notNull()); + CHECK(c->isValid()); + CHECK(a.get() == b.get()); + CHECK(a.get() != c.get()); + } + + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: loadFace on a real file succeeds and getName is its path", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + LLPointer ft = loadFt(path); + REQUIRE(ft.notNull()); + CHECK(ft->getName() == path); + } + + // loadFace on a missing file returns false; callers that own the + // LLFontFreetype must see a safe-to-destruct instance. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: loadFace on a missing file fails and the instance destructs cleanly", "[LLFontFreetype]") + { + LLPointer ft = new LLFontFreetype; + const bool ok = ft->loadFace("does/not/exist.ttf", + 14.f, 96.f, 96.f, true, 0, + EFontHinting::DEFAULT, 0); + CHECK_FALSE(ok); + // Don't deref ft beyond this — destructor must run cleanly. + ft = nullptr; + UNSCOPED_INFO("destruct after failed load did not crash"); + CHECK(true); + } + + // getCharGlyphIndex returns non-zero for ASCII present in the cmap + // and zero for codepoints absent from the cmap. Second call hits + // the cache (no observable difference, but exercises the path). + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: getCharGlyphIndex finds ASCII and is 0 for a private-use codepoint", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + LLPointer ft = loadFt(path); + REQUIRE(ft.notNull()); + const U32 a1 = ft->getCharGlyphIndex(L'A'); + const U32 a2 = ft->getCharGlyphIndex(L'A'); + CHECK(a1 != 0u); + UNSCOPED_INFO("a repeat lookup hits the cache"); + CHECK(a1 == a2); + // U+E000 is in the BMP private-use area — DejaVuSans doesn't + // populate it; getCharGlyphIndex must return 0 (cached miss + // is meaningful — it lets the renderer fall through to + // fallbacks without re-walking the cmap). + CHECK(ft->getCharGlyphIndex(0xE000) == 0u); + } + + // faceHasGlyph mirrors getCharGlyphIndex's notion of "covers this + // codepoint." Verify positive (ASCII present) and negative + // (Noto-COLRv1 lacks U+FE0F) cases — pins the VS-16 strip + // discriminator used by shape_sub_run. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: faceHasGlyph: DejaVuSans has 'a'; Noto-COLRv1 has ZWJ but not U+FE0F", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string noto = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(dejavu) || !fileExists(noto)) + SKIP("DejaVuSans.woff2 + Noto-COLRv1.ttf both required"); + + LLPointer dv = loadFt(dejavu); + LLPointer nt = loadFt(noto); + REQUIRE(dv.notNull()); + REQUIRE(nt.notNull()); + CHECK(dv->faceHasGlyph(L'a')); + CHECK_FALSE(nt->faceHasGlyph(0xFE0F)); + UNSCOPED_INFO("U+200D is ZWJ"); + CHECK(nt->faceHasGlyph(0x200D)); + } + + // useSubpixelPen depends on hinting and color/svg flags: + // FORCE_AUTOHINT on a non-color font => true; DEFAULT hinting => false; + // a color font (regardless of hinting) => false. Pins the rule at + // alfontface.cpp:99. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: useSubpixelPen only for an autohinted face without color", "[LLFontFreetype]") + { + const std::string path = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(path)) + SKIP("DejaVuSans.woff2 not present"); + + LLPointer a = loadFt(path, /*is_fallback=*/true, + 14.f, -1, EFontHinting::DEFAULT); + LLPointer b = loadFt(path, /*is_fallback=*/true, + 14.f, -1, EFontHinting::FORCE_AUTOHINT); + REQUIRE(a.notNull()); + REQUIRE(b.notNull()); + UNSCOPED_INFO("DEFAULT hinting"); + CHECK_FALSE(a->useSubpixelPen()); + UNSCOPED_INFO("FORCE_AUTOHINT on a font without color"); + CHECK(b->useSubpixelPen()); + + // A color font (Noto-COLRv1) with FORCE_AUTOHINT still reports + // false because the rule short-circuits on color/svg. + const std::string colr = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (fileExists(colr)) + { + LLPointer c = loadFt(colr, true, 14.f, -1, + EFontHinting::FORCE_AUTOHINT); + REQUIRE(c.notNull()); + UNSCOPED_INFO("FORCE_AUTOHINT on a color font"); + CHECK_FALSE(c->useSubpixelPen()); + } + } + + // isFixedWidth: DejaVuSansMono yes, DejaVuSans no. + // getAllowMonospaceLigatures defaults false; setter toggles. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: isFixedWidth only for DejaVuSansMono; monospace ligatures off until set", "[LLFontFreetype]") + { + const std::string mono = std::string(kFontDir) + "DejaVuSansMono.woff2"; + const std::string prop = std::string(kFontDir) + "DejaVuSans.woff2"; + if (!fileExists(mono) || !fileExists(prop)) + SKIP("DejaVuSans + DejaVuSansMono required"); + + LLPointer m = loadFt(mono); + LLPointer p = loadFt(prop); + CHECK(m->isFixedWidth()); + CHECK_FALSE(p->isFixedWidth()); + UNSCOPED_INFO("ligatures default off"); + CHECK_FALSE(m->getAllowMonospaceLigatures()); + m->setAllowMonospaceLigatures(true); + CHECK(m->getAllowMonospaceLigatures()); + m->setAllowMonospaceLigatures(false); + CHECK_FALSE(m->getAllowMonospaceLigatures()); + } + + // getStyle reflects the FT face's intrinsic flags. DejaVuSans is + // NORMAL; DejaVuSans-Bold has the BOLD bit set after load. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: getStyle has BOLD for DejaVuSans-Bold and not for DejaVuSans", "[LLFontFreetype]") + { + const std::string normal = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string bold = std::string(kFontDir) + "DejaVuSans-Bold.woff2"; + if (!fileExists(normal) || !fileExists(bold)) + SKIP("DejaVuSans + DejaVuSans-Bold required"); + + LLPointer n = loadFt(normal); + LLPointer b = loadFt(bold); + CHECK((n->getStyle() & LLFontGL::BOLD) == 0); + CHECK((b->getStyle() & LLFontGL::BOLD) != 0); + } + + // selectShapingFace walks the fallback chain by codepoint coverage: + // ASCII routes to the head, CJK routes to a CJK fallback, emoji + // routes to the emoji fallback. Pins multi-hop priority resolution + // at llfontfreetype.cpp:266-284. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: selectShapingFace sends ASCII to the head and CJK and emoji to fallbacks", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string han = std::string(kFontDir) + "SourceHanSans-Regular.woff2"; + const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(dejavu) || !fileExists(han) || !fileExists(emoji)) + SKIP("DejaVuSans + SourceHanSans + Noto-COLRv1 required"); + + LLPointer head = loadFt(dejavu); + LLPointer cjk = loadFt(han); + LLPointer emo = loadFt(emoji); + REQUIRE(head.notNull()); + REQUIRE(cjk.notNull()); + REQUIRE(emo.notNull()); + head->addFallbackFont(cjk); + head->addFallbackFont(emo); + + U32 idx = 0; + // ASCII 'a' lives in the head's cmap → head wins. + CHECK(head->selectShapingFace(L'a', idx) == head.get()); + UNSCOPED_INFO("the head's glyph for 'a'"); + CHECK(idx != 0u); + + // U+4F60 你 lives in SourceHanSans, not in DejaVuSans → CJK fallback. + idx = 0; + CHECK(head->selectShapingFace(0x4F60, idx) == cjk.get()); + + // U+1F525 (fire emoji) lives in Noto-COLRv1, not in either text + // face → emoji fallback wins. + idx = 0; + CHECK(head->selectShapingFace(0x1F525, idx) == emo.get()); + } + + // mShapingFaceResolution caches selectShapingFace decisions; adding + // a new fallback must invalidate that cache so a previously-resolved + // codepoint can re-route through the newly-attached chain. Pins the + // cache-clear on addFallbackFont at llfontfreetype.cpp:434. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: addFallbackFont clears the faces selectShapingFace has cached", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string emoji = std::string(kFontDir) + "Noto-COLRv1.ttf"; + if (!fileExists(dejavu) || !fileExists(emoji)) + SKIP("DejaVuSans + Noto-COLRv1 required"); + + LLPointer head = loadFt(dejavu); + LLPointer emo = loadFt(emoji); + REQUIRE(head.notNull()); + REQUIRE(emo.notNull()); + + // First lookup, no fallback registered. 'a' resolves to head; + // emoji resolves to head with idx=0 (no fallback covers it). + U32 idx_a1 = 0, idx_e1 = 0; + const LLFontFreetype* a1 = head->selectShapingFace(L'a', idx_a1); + const LLFontFreetype* e1 = head->selectShapingFace(0x1F525, idx_e1); + CHECK(a1 == head.get()); + { + INFO("emoji with no fallback yet"); + CHECK(e1 == head.get()); + CHECK(idx_e1 == 0u); + } + + // Second lookup hits the cache and returns the same pointer. + U32 idx_a2 = 0; + UNSCOPED_INFO("a repeat lookup hits the cache"); + CHECK(head->selectShapingFace(L'a', idx_a2) == head.get()); + + // Add the emoji fallback. The cache must invalidate so the next + // emoji lookup re-runs the walker through the new chain. + head->addFallbackFont(emo); + + U32 idx_e2 = 0; + CHECK(head->selectShapingFace(0x1F525, idx_e2) == emo.get()); + } + + // Functor gating: a fallback whose char_functor returns false for + // codepoint X is excluded from X's resolution, even if its cmap + // covers X. Pins the must-accept gate at llfontfreetype.cpp:268-273. + TEST_CASE_METHOD(llfontfreetype_data, "LLFontFreetype: a fallback's char functor keeps it from the codepoints it rejects", "[LLFontFreetype]") + { + const std::string dejavu = std::string(kFontDir) + "DejaVuSans.woff2"; + const std::string emoji = std::string(kFontDir) + "TwemojiSVG.woff2"; + if (!fileExists(dejavu) || !fileExists(emoji)) + SKIP("DejaVuSans + TwemojiSVG required"); + + LLPointer head = loadFt(dejavu); + LLPointer emo = loadFt(emoji); + REQUIRE(head.notNull()); + REQUIRE(emo.notNull()); + + // Functor accepts only emoji-range codepoints; rejects ASCII. + // If TwemojiSVG happens to carry ASCII outlines, the functor + // forces the head to win 'a' regardless. + auto pictograph_only = [](llwchar c) -> bool { return c >= 0x1F000; }; + head->addFallbackFont(emo, pictograph_only); + + U32 idx = 0; + UNSCOPED_INFO("the functor rejects 'a' for Twemoji"); + CHECK(head->selectShapingFace(L'a', idx) == head.get()); + // Functor allows emoji range, so emoji codepoints route to Twemoji. + idx = 0; + UNSCOPED_INFO("the functor passes the emoji to Twemoji"); + CHECK(head->selectShapingFace(0x1F525, idx) == emo.get()); } } diff --git a/indra/llrender/tests/llfontgl_test.cpp b/indra/llrender/tests/llfontgl_test.cpp index b95af706c9..044359ca6f 100644 --- a/indra/llrender/tests/llfontgl_test.cpp +++ b/indra/llrender/tests/llfontgl_test.cpp @@ -41,7 +41,7 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include @@ -71,8 +71,6 @@ namespace } } -namespace tut -{ namespace { // The shared GL context. TUT spins fixtures per-test method, but @@ -93,21 +91,16 @@ namespace ~llfontgl_data() = default; }; - typedef test_group llfontgl_test; - typedef llfontgl_test::object llfontgl_object; - tut::llfontgl_test llfontgl_testcase("LLFontGL"); -} // initClass through the explicit-path overload should bring up a // working font registry under a real GL context. loadDefaultFonts // (called from initClass) returns true only if every required // family resolved through to a usable LLFontFreetype. - template<> template<> - void llfontgl_object::test<1>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: initClass from an explicit fonts.xml brings up a working registry", "[LLFontGL]") { if (!fileExists(kFontsXml)) { - skip("fonts.xml not found at " + std::string(kFontsXml)); + SKIP("fonts.xml not found at " + std::string(kFontsXml)); return; } @@ -118,22 +111,19 @@ namespace /*font_overrides=*/LLSD(), /*create_gl_textures=*/true); - ensure("default fonts loaded", - LLFontGL::loadDefaultFonts()); - ensure("SansSerif resolves", - LLFontGL::getFontSansSerif() != nullptr); + CHECK(LLFontGL::loadDefaultFonts()); + CHECK(LLFontGL::getFontSansSerif() != nullptr); } // After ASCII rasterization, the SansSerif font's bitmap cache // should have at least one greyscale page with a live GL texture. // Catches the silent-no-op failure mode where the GL context is // healthy but createGLTexture skipped the upload. - template<> template<> - void llfontgl_object::test<2>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: ASCII glyphs land on a grayscale atlas page with a live GL texture", "[LLFontGL]") { if (!fileExists(kFontsXml)) { - skip("fonts.xml not found at " + std::string(kFontsXml)); + SKIP("fonts.xml not found at " + std::string(kFontsXml)); return; } @@ -145,7 +135,7 @@ namespace LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("SansSerif resolves", font != nullptr); + REQUIRE(font != nullptr); // Force the ASCII range to rasterize so the atlas isn't empty. // (createFont's pre-warm in getFont already does this on first @@ -153,116 +143,106 @@ namespace font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("LLFontFreetype present", ft != nullptr); + REQUIRE(ft != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("at least one grayscale atlas page", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + REQUIRE(cache != nullptr); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("page 0 LLImageGL present", page != nullptr); - ensure("page 0 has a live GL texture name", - page->getTexName() != 0); + REQUIRE(page != nullptr); + CHECK(page->getTexName() != 0); } // getFontMonospace resolves and reports fixed-width through its // underlying freetype. Pins the registry's monospace family // routing — a regression that resolved Monospace to a // proportional fallback would surface here. - template<> template<> - void llfontgl_object::test<3>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontMonospace resolves to a fixed-width face", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* mono = LLFontGL::getFontMonospace(); - ensure("Monospace resolves", mono != nullptr); + REQUIRE(mono != nullptr); const LLFontFreetype* ft = mono->getFontFreetype(); - ensure("Monospace has a freetype", ft != nullptr); - ensure("Monospace freetype is fixed-width: " + ft->getName(), - ft->isFixedWidth()); + REQUIRE(ft != nullptr); + INFO("Monospace resolved to " << ft->getName()); + CHECK(ft->isFixedWidth()); } // getFontSansSerifBig resolves with positive line-height. // Captures the registry's size-suffix routing — Big size must // produce a freetype whose metrics scale up from the base. - template<> template<> - void llfontgl_object::test<4>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontSansSerifBig has a taller line than getFontSansSerifSmall", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* small = LLFontGL::getFontSansSerifSmall(); LLFontGL* big = LLFontGL::getFontSansSerifBig(); - ensure("Small SansSerif resolves", small != nullptr); - ensure("Big SansSerif resolves", big != nullptr); + REQUIRE(small != nullptr); + REQUIRE(big != nullptr); const LLFontFreetype* fs = small->getFontFreetype(); const LLFontFreetype* fb = big->getFontFreetype(); - ensure("Small line-height > 0", fs->getLineHeight() > 0.f); - ensure("Big line-height > 0", fb->getLineHeight() > 0.f); - ensure("Big line-height > Small line-height", - fb->getLineHeight() > fs->getLineHeight()); + CHECK(fs->getLineHeight() > 0.f); + CHECK(fb->getLineHeight() > 0.f); + CHECK(fb->getLineHeight() > fs->getLineHeight()); } // Repeat-call getter caches: getFontSansSerif() returns the same // pointer on second call. Pins the per-getter static cache — // a regression that re-resolved on every call would change the // pointer (and waste a registry lookup per UI label render). - template<> template<> - void llfontgl_object::test<5>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontSansSerif returns the same font on every call", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* a = LLFontGL::getFontSansSerif(); LLFontGL* b = LLFontGL::getFontSansSerif(); - ensure("first resolve non-null", a != nullptr); - ensure_equals("second resolve returns the same instance", a, b); + REQUIRE(a != nullptr); + CHECK(a == b); } // generateASCIIglyphs is idempotent — repeat calls don't allocate // additional atlas sheets. Pins the head-cache lookup that makes // the second call a fast no-op. - template<> template<> - void llfontgl_object::test<6>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a second generateASCIIglyphs adds no atlas sheets", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); const LLFontBitmapCache* bc = ft->getFontBitmapCache(); const U32 sheets_after_first = bc->getNumBitmaps(EFontGlyphType::Grayscale); - ensure("at least one sheet after first generate", - sheets_after_first >= 1u); + CHECK(sheets_after_first >= 1u); font->generateASCIIglyphs(); const U32 sheets_after_second = bc->getNumBitmaps(EFontGlyphType::Grayscale); - ensure_equals("second generate is idempotent (no extra sheets)", - sheets_after_second, sheets_after_first); + CHECK(sheets_after_second == sheets_after_first); } // getFontByName returns the same instance for the same legacy // name string. Pins the legacy-name routing in the registry. - template<> template<> - void llfontgl_object::test<7>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getFontByName returns the same instance for the same legacy name", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* a = LLFontGL::getFontByName("SANSSERIF"); LLFontGL* b = LLFontGL::getFontByName("SANSSERIF"); - ensure("legacy name resolves to non-null", a != nullptr); - ensure_equals("repeat lookup returns same instance", a, b); + REQUIRE(a != nullptr); + CHECK(a == b); } // collectGarbage on the font face does not crash and (since @@ -270,17 +250,16 @@ namespace // are recently used. Pins 169390b593 (cache-sweep moved off // hot path) and 7c05b2de82 (collectGarbage safe at any frame // boundary). - template<> template<> - void llfontgl_object::test<8>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: collectGarbage leaves glyphs in use and their atlas page alone", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("freetype present", ft != nullptr); + REQUIRE(ft != nullptr); // What a sweep must not do is disturb glyphs still in use. Measure // first, sweep twice -- the second is inside the throttle interval and @@ -291,37 +270,39 @@ namespace const std::string sample = "The quick brown fox"; font->generateASCIIglyphs(); const F32 before = font->getWidthF32Bytes(sample, 0, (S32)sample.size(), false); - ensure("sample has a width to begin with", before > 0.f); + UNSCOPED_INFO("sample has a width to begin with"); + CHECK(before > 0.f); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); + REQUIRE(cache != nullptr); const U32 pages_before = cache->getNumBitmaps(EFontGlyphType::Grayscale); ft->collectGarbage(); ft->collectGarbage(); const F32 after = font->getWidthF32Bytes(sample, 0, (S32)sample.size(), false); - ensure_equals("a sweep does not change what live text measures", - before, after); - ensure_equals("a sweep does not drop a page holding live glyphs", - cache->getNumBitmaps(EFontGlyphType::Grayscale), pages_before); + UNSCOPED_INFO("a sweep does not change what live text measures"); + CHECK(before == after); + UNSCOPED_INFO("a sweep does not drop a page holding live glyphs"); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) == pages_before); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("page 0 still present after a sweep", page != nullptr); - ensure("page 0 still has a live GL texture", page->getTexName() != 0); + UNSCOPED_INFO("page 0 still present after a sweep"); + REQUIRE(page != nullptr); + UNSCOPED_INFO("page 0 still has a live GL texture"); + CHECK(page->getTexName() != 0); } // Re-calling initClass after the registry is up is a no-op // (warns + bails). The registry pointer must remain stable so // LLFontGL's static fontp caches don't dangle. - template<> template<> - void llfontgl_object::test<9>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a second initClass leaves the fonts already resolved in place", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* before = LLFontGL::getFontSansSerif(); - ensure("first resolve non-null", before != nullptr); + REQUIRE(before != nullptr); // Re-call initClass with different DPI. The single-shot // guard short-circuits, so the registry stays the same and @@ -329,87 +310,124 @@ namespace LLFontGL::initClass(72.f, 0.5f, 0.5f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* after = LLFontGL::getFontSansSerif(); - ensure_equals("second resolve returns same instance after re-init", - after, before); + CHECK(after == before); + } + + // A second initClass through the explicit-path overload is ignored + // whole: the fonts already made keep the DPI and scale they were made + // at, which every later layout and measurement reads. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a second initClass leaves the DPI and scale the fonts were made at", "[LLFontGL]") + { + if (!fileExists(kFontsXml)) + SKIP("fonts.xml not found"); + LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + const F32 horiz_dpi = LLFontGL::sHorizDPI; + const F32 vert_dpi = LLFontGL::sVertDPI; + const F32 scale_x = LLFontGL::sScaleX; + const F32 scale_y = LLFontGL::sScaleY; + + LLFontGL::initClass(72.f, 0.5f, 0.5f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + CHECK(LLFontGL::sHorizDPI == horiz_dpi); + CHECK(LLFontGL::sVertDPI == vert_dpi); + CHECK(LLFontGL::sScaleX == scale_x); + CHECK(LLFontGL::sScaleY == scale_y); + } + + // Static getter family — every named getter must resolve to a + // non-null LLFontGL after a successful initClass. Smoke check + // that the default fonts.xml covers all the families exposed. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: every named font getter resolves after initClass", "[LLFontGL]") + { + if (!fileExists(kFontsXml)) + SKIP("fonts.xml not found"); + LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + CHECK(LLFontGL::getFontSansSerif() != nullptr); + CHECK(LLFontGL::getFontSansSerifSmall() != nullptr); + CHECK(LLFontGL::getFontSansSerifSmallBold() != nullptr); + CHECK(LLFontGL::getFontSansSerifBig() != nullptr); + CHECK(LLFontGL::getFontSansSerifHuge() != nullptr); + CHECK(LLFontGL::getFontSansSerifBold() != nullptr); + CHECK(LLFontGL::getFontMonospace() != nullptr); + CHECK(LLFontGL::getFontEmojiSmall() != nullptr); + CHECK(LLFontGL::getFontEmojiMedium() != nullptr); + CHECK(LLFontGL::getFontEmojiLarge() != nullptr); + CHECK(LLFontGL::getFontDefault() != nullptr); } // getWidthF32: the whole-string overload defaults to the text's length; // a getWidthF32Bytes call with an explicit budget matches it. - template<> template<> - void llfontgl_object::test<11>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getWidthF32 of a whole string equals getWidthF32Bytes of all of it", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; const F32 wfull = font->getWidthF32(s); const F32 wexp = font->getWidthF32Bytes(s, 0, (S32)s.size(), false); - ensure("getWidthF32 with default max == explicit max", - wfull == wexp); - ensure("width is positive", wfull > 0.f); + UNSCOPED_INFO("getWidthF32 with default max == explicit max"); + CHECK(wfull == wexp); + CHECK(wfull > 0.f); } // Width monotonicity: width(AAA) > width(AA) > width(A). Used by // render()'s halign math at llfontgl.cpp:354-372 — the alignment // calc relies on accumulated width strictly increasing with // character count for the same string. - template<> template<> - void llfontgl_object::test<12>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a string grows wider with each character added", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const F32 w1 = font->getWidthF32(std::string("A")); const F32 w2 = font->getWidthF32(std::string("AA")); const F32 w3 = font->getWidthF32(std::string("AAA")); - ensure("width(A) > 0", w1 > 0.f); - ensure("width(AA) > width(A)", w2 > w1); - ensure("width(AAA) > width(AA)", w3 > w2); + CHECK(w1 > 0.f); + CHECK(w2 > w1); + CHECK(w3 > w2); } // Ellipsis budget: getWidthF32("....") is positive — render() pre- // computes this for the truncation path (llfontgl.cpp:459 references // the dots constant). A regression where '.' rasterized to a 0-width // glyph would silently break ellipsis sizing. - template<> template<> - void llfontgl_object::test<13>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getWidthF32 gives the ellipsis dots a positive width", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const F32 w = font->getWidthF32(std::string("....")); - ensure("ellipsis width is positive", w > 0.f); + CHECK(w > 0.f); } // maxDrawableBytes boundary: zero pixel budget fits nothing, // F32_MAX fits all, mid-budget fits a partial prefix. - template<> template<> - void llfontgl_object::test<14>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: maxDrawableBytes fits none or all or the prefix the budget holds", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; - ensure_equals("budget 0 fits nothing", - font->maxDrawableBytes(s, 0.f, (S32)s.size()), + CHECK(font->maxDrawableBytes(s, 0.f, (S32)s.size()) == (S32)0); - ensure_equals("budget F32_MAX fits the whole string", - font->maxDrawableBytes(s, F32_MAX, - (S32)s.size()), + CHECK(font->maxDrawableBytes(s, F32_MAX, + (S32)s.size()) == (S32)s.size()); // Find a budget that should fit "Hel" but not "Hell". const F32 w_hel = font->getWidthF32Bytes(s, 0, 3, false); @@ -418,22 +436,21 @@ namespace const F32 mid_budget = (w_hel + w_hell) * 0.5f; const S32 fit = font->maxDrawableBytes(s, mid_budget, (S32)s.size()); - ensure("partial-budget fit is in [3, 3] (Hel exactly)", - fit == 3); + UNSCOPED_INFO("a budget halfway between Hel and Hell fits Hel"); + CHECK(fit == 3); } // byteFromPixelOffset round-trip: for each prefix length i, the // pixel offset corresponding to the prefix's width must invert to // i. Width accumulation and pixel->offset lookup must agree. - template<> template<> - void llfontgl_object::test<15>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: byteFromPixelOffset inverts getWidthF32Bytes at every character start", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; // ASCII, so one byte per character: prefix lengths 0..5. for (S32 i = 0; i <= (S32)s.size(); ++i) @@ -448,8 +465,8 @@ namespace // character wide -- it accepts precisely the mistake this is here // to catch -- and the target is a boundary, which rounding takes // to the boundary itself. - ensure_equals("round-trip lands on the offset it was measured from", - cp, i); + UNSCOPED_INFO("round-trip lands on the offset it was measured from"); + REQUIRE(cp == i); } // The same round trip where a character is not a byte. The expected @@ -461,7 +478,8 @@ namespace const F32 w = font->getWidthF32Bytes(m, 0, start, false); const S32 got = font->byteFromPixelOffset(m, 0, w, F32_MAX, (S32)m.size(), /*round=*/true); - ensure_equals("round-trip through a multi-byte character", got, start); + UNSCOPED_INFO("round-trip through a multi-byte character"); + REQUIRE(got == start); } } @@ -470,15 +488,14 @@ namespace // width measurement boundary — a regression that shaped "a你" // against DejaVu only (replacing 你 with notdef) would diverge. // Skip if the default fallback chain doesn't cover U+4F60. - template<> template<> - void llfontgl_object::test<16>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a mixed-script string is as wide as its parts", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string a_s = "a"; const std::string c_s = utf8str_from_cp(0x4F60); @@ -488,11 +505,50 @@ namespace const F32 wc = font->getWidthF32(c_s); const F32 wac = font->getWidthF32(ac_s); if (wc == 0.f) - skip("font's fallback chain doesn't cover U+4F60"); + SKIP("font's fallback chain doesn't cover U+4F60"); // Tolerance: ~0.5 px accounts for sub-pixel positioning across // shape-range boundaries. - ensure("width(a你) ≈ width(a) + width(你) within 0.5px", - std::abs(wac - (wa + wc)) <= 0.5f); + CHECK(std::abs(wac - (wa + wc)) <= 0.5f); + } + + // Style string parser round-trip: every output of getStringFromStyle + // must round-trip through getStyleFromString. The legacy parser used + // substring search, so a poisoned token like "FAUX-BOLD" set BOLD; + // the new tokenizer-based parser splits on '|' and matches whole + // tokens. NORMAL has its own dedicated string ("NORMAL", no leading + // pipe), so the round-trip pins both edge cases at once. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: getStringFromStyle round-trips and the parser matches whole tokens", "[LLFontGL]") + { + const U8 cases[] = { + LLFontGL::NORMAL, + LLFontGL::BOLD, + LLFontGL::ITALIC, + LLFontGL::UNDERLINE, + LLFontGL::BOLD | LLFontGL::ITALIC, + LLFontGL::BOLD | LLFontGL::UNDERLINE, + LLFontGL::ITALIC | LLFontGL::UNDERLINE, + LLFontGL::BOLD | LLFontGL::ITALIC | LLFontGL::UNDERLINE, + }; + for (U8 style : cases) + { + const std::string s = LLFontGL::getStringFromStyle(style); + CAPTURE(s); + REQUIRE_FALSE(s.empty()); + REQUIRE(s.front() != '|'); + REQUIRE(s.back() != '|'); + REQUIRE(LLFontGL::getStyleFromString(s) == style); + } + + // Substring-match regression guard: legacy parser matched + // "BOLD" inside "FAUX-BOLD"; new parser tokenizes on '|'. + UNSCOPED_INFO("FAUX-BOLD does not set BOLD"); + CHECK(LLFontGL::getStyleFromString("FAUX-BOLD") == 0); + UNSCOPED_INFO("NOTBOLD does not set BOLD"); + CHECK(LLFontGL::getStyleFromString("NOTBOLD") == 0); + CHECK(LLFontGL::getStyleFromString("") == + LLFontGL::NORMAL); + CHECK(LLFontGL::getStyleFromString("BOLD|ITALIC") == + (LLFontGL::BOLD | LLFontGL::ITALIC)); } // byteFromPixelOffset must snap to cluster boundaries on a @@ -504,15 +560,14 @@ namespace // selection rect's right edge across the cluster's interior — // observed as drag-select highlight flicker on ZWJ flags / family // emoji where 🐶 (1 codepoint) is unaffected. - template<> template<> - void llfontgl_object::test<18>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: byteFromPixelOffset never lands inside a ZWJ emoji cluster", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); // "X" + trans flag (5 codepoints) + "Y". The cluster's byte bounds // are recorded as the string is built rather than counted by hand: @@ -531,7 +586,7 @@ namespace const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("emoji fallback not available"); + SKIP("emoji fallback not available"); // Sanity: width up to the cluster's end must exceed width up to its // start by at least a few pixels — otherwise the emoji didn't @@ -540,7 +595,7 @@ namespace const F32 w_pre = font->getWidthF32Bytes(s, 0, cluster_start, false); const F32 w_post = font->getWidthF32Bytes(s, 0, cluster_end, false); if (w_post - w_pre < 2.f) - skip("emoji cluster did not shape to a measurable glyph"); + SKIP("emoji cluster did not shape to a measurable glyph"); // Walk target_x across the rendered cluster's pixel span at // sub-pixel granularity. Every returned cursor pos must land on a @@ -553,78 +608,13 @@ namespace const S32 pos = font->byteFromPixelOffset(s, 0, x, F32_MAX, total, /*round=*/true); + CAPTURE(x, pos); const bool inside_cluster = (pos > cluster_start && pos < cluster_end); - ensure("byteFromPixelOffset round=true returns mid-cluster offset", - !inside_cluster); + UNSCOPED_INFO("byteFromPixelOffset round=true returns mid-cluster offset"); + REQUIRE_FALSE(inside_cluster); } } - // Static getter family — every named getter must resolve to a - // non-null LLFontGL after a successful initClass. Smoke check - // that the default fonts.xml covers all the families exposed. - template<> template<> - void llfontgl_object::test<10>() - { - if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); - LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, - LLSD(), /*create_gl_textures=*/true); - ensure("SansSerif", LLFontGL::getFontSansSerif() != nullptr); - ensure("SansSerifSmall", LLFontGL::getFontSansSerifSmall() != nullptr); - ensure("SansSerifSmallBold", LLFontGL::getFontSansSerifSmallBold() != nullptr); - ensure("SansSerifBig", LLFontGL::getFontSansSerifBig() != nullptr); - ensure("SansSerifHuge", LLFontGL::getFontSansSerifHuge() != nullptr); - ensure("SansSerifBold", LLFontGL::getFontSansSerifBold() != nullptr); - ensure("Monospace", LLFontGL::getFontMonospace() != nullptr); - ensure("EmojiSmall", LLFontGL::getFontEmojiSmall() != nullptr); - ensure("EmojiMedium", LLFontGL::getFontEmojiMedium() != nullptr); - ensure("EmojiLarge", LLFontGL::getFontEmojiLarge() != nullptr); - ensure("FontDefault", LLFontGL::getFontDefault() != nullptr); - } - - // Style string parser round-trip: every output of getStringFromStyle - // must round-trip through getStyleFromString. The legacy parser used - // substring search, so a poisoned token like "FAUX-BOLD" set BOLD; - // the new tokenizer-based parser splits on '|' and matches whole - // tokens. NORMAL has its own dedicated string ("NORMAL", no leading - // pipe), so the round-trip pins both edge cases at once. - template<> template<> - void llfontgl_object::test<17>() - { - const U8 cases[] = { - LLFontGL::NORMAL, - LLFontGL::BOLD, - LLFontGL::ITALIC, - LLFontGL::UNDERLINE, - LLFontGL::BOLD | LLFontGL::ITALIC, - LLFontGL::BOLD | LLFontGL::UNDERLINE, - LLFontGL::ITALIC | LLFontGL::UNDERLINE, - LLFontGL::BOLD | LLFontGL::ITALIC | LLFontGL::UNDERLINE, - }; - for (U8 style : cases) - { - const std::string s = LLFontGL::getStringFromStyle(style); - ensure("string output is non-empty", !s.empty()); - ensure("no leading pipe", s.front() != '|'); - ensure("no trailing pipe", s.back() != '|'); - ensure_equals("round-trip preserves style bits", - LLFontGL::getStyleFromString(s), style); - } - - // Substring-match regression guard: legacy parser matched - // "BOLD" inside "FAUX-BOLD"; new parser tokenizes on '|'. - ensure_equals("FAUX-BOLD does not set BOLD", - LLFontGL::getStyleFromString("FAUX-BOLD"), 0); - ensure_equals("NOTBOLD does not set BOLD", - LLFontGL::getStyleFromString("NOTBOLD"), 0); - ensure_equals("empty string is NORMAL", - LLFontGL::getStyleFromString(""), - LLFontGL::NORMAL); - ensure_equals("BOLD|ITALIC tokenizes correctly", - LLFontGL::getStyleFromString("BOLD|ITALIC"), - LLFontGL::BOLD | LLFontGL::ITALIC); - } - // firstDrawableByte is the only one of the three layout walks that goes // backward, and a backward walk is where a cluster gets split: the // trailing codepoints of one carry no advance of their own, so a naive @@ -632,15 +622,14 @@ namespace // width. Sweep the budget across a string with a ZWJ cluster in it and // hold every answer to the promise llfontgl.h makes -- a character start, // and where the text shapes, a cluster start. - template<> template<> - void llfontgl_object::test<19>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: firstDrawableByte never lands inside a character or a ZWJ cluster", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); // Byte bounds recorded as the string is built: the five codepoints of // a trans flag are 4/3/3/3/3 bytes, so a hand-written offset here @@ -658,22 +647,26 @@ namespace const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("emoji fallback not available"); + SKIP("emoji fallback not available"); const F32 w_pre = font->getWidthF32Bytes(s, 0, cluster_start, false); const F32 w_post = font->getWidthF32Bytes(s, 0, cluster_end, false); if (w_post - w_pre < 2.f) - skip("emoji cluster did not shape to a measurable glyph"); + SKIP("emoji cluster did not shape to a measurable glyph"); for (F32 px = 0.f; px <= wfull + 2.f; px += 0.5f) { const S32 first = font->firstDrawableByte(s, px); - ensure("firstDrawableByte names a position inside the text", - first >= 0 && first < total); - ensure("firstDrawableByte returns a continuation byte", - ((unsigned char)s[first] & 0xC0) != 0x80); + CAPTURE(px, first); + { + INFO("firstDrawableByte names a position inside the text"); + REQUIRE(first >= 0); + REQUIRE(first < total); + } + UNSCOPED_INFO("firstDrawableByte returns a continuation byte"); + REQUIRE(((unsigned char)s[first] & 0xC0) != 0x80); const bool inside_cluster = (first > cluster_start && first < cluster_end); - ensure("firstDrawableByte returns a position inside a cluster", - !inside_cluster); + UNSCOPED_INFO("firstDrawableByte returns a position inside a cluster"); + REQUIRE_FALSE(inside_cluster); } } @@ -681,180 +674,181 @@ namespace // set of positions a full sweep can return has to be exactly the set of // character starts: anything missing is a character the backward walk // stepped over, and anything extra is an offset inside one. - template<> template<> - void llfontgl_object::test<20>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: firstDrawableByte sweeps through every character start and no other", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); // "a", e-acute (two bytes), "b", "c" -- so byte 2 is the only offset // in the string that is not somewhere drawing may begin. const std::string s = "a\xC3\xA9" "bc"; const S32 total = (S32)s.size(); - ensure_equals("the fixture string is five bytes", total, (S32)5); + UNSCOPED_INFO("the fixture string is five bytes"); + CHECK(total == (S32)5); const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("font produced no width"); + SKIP("font produced no width"); std::set seen; S32 previous = total; for (F32 px = 0.f; px <= wfull + 2.f; px += 0.25f) { const S32 first = font->firstDrawableByte(s, px); - ensure("a larger budget starts drawing later", first <= previous); + CAPTURE(px); + UNSCOPED_INFO("a larger budget starts drawing no later"); + REQUIRE(first <= previous); previous = first; seen.insert(first); } const std::set starts{ 0, 1, 3, 4 }; - ensure("the sweep reaches every character start and nothing else", - seen == starts); + UNSCOPED_INFO("the sweep reaches every character start and nothing else"); + CHECK(seen == starts); } // The edges, and the one case with no good answer. A budget that cannot // hold even the last character still has to name a place to draw from -- // that character, clipped -- because returning nothing would leave a // caller scrolling a field it can never make progress in. - template<> template<> - void llfontgl_object::test<21>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: firstDrawableByte on empty text and at zero and unlimited budgets", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; const S32 total = (S32)s.size(); const F32 wfull = font->getWidthF32Bytes(s, 0, total, false); if (wfull <= 0.f) - skip("font produced no width"); + SKIP("font produced no width"); - ensure_equals("empty text draws from the beginning", - font->firstDrawableByte(std::string_view(), 100.f), (S32)0); - ensure_equals("room for everything draws from the beginning", - font->firstDrawableByte(s, F32_MAX), (S32)0); - ensure_equals("no room at all still draws the last character", - font->firstDrawableByte(s, 0.f), total - 1); - ensure_equals("a byte budget of nothing draws from the beginning", - font->firstDrawableByte(s, F32_MAX, S32_MAX, 0), (S32)0); + UNSCOPED_INFO("empty text draws from the beginning"); + CHECK(font->firstDrawableByte(std::string_view(), 100.f) == (S32)0); + UNSCOPED_INFO("room for everything draws from the beginning"); + CHECK(font->firstDrawableByte(s, F32_MAX) == (S32)0); + UNSCOPED_INFO("no room at all still draws the last character"); + CHECK(font->firstDrawableByte(s, 0.f) == total - 1); + UNSCOPED_INFO("a byte budget of nothing draws from the beginning"); + CHECK(font->firstDrawableByte(s, F32_MAX, S32_MAX, 0) == (S32)0); // start_pos chooses which character has to be the last one drawn, so // with room to spare the answer is the beginning whichever it is. for (S32 start = 0; start < total; ++start) { - ensure_equals("room for everything draws from the beginning", - font->firstDrawableByte(s, F32_MAX, start), (S32)0); - ensure_equals("no room draws the character asked for", - font->firstDrawableByte(s, 0.f, start), start); + CAPTURE(start); + UNSCOPED_INFO("room for everything draws from the beginning"); + REQUIRE(font->firstDrawableByte(s, F32_MAX, start) == (S32)0); + UNSCOPED_INFO("no room draws the character asked for"); + REQUIRE(font->firstDrawableByte(s, 0.f, start) == start); } } // maxDrawableBytes over text where a character is not a byte. The ASCII // test beside this one cannot tell a byte count from a character count, a // glyph count or a cluster count -- they are the same number there. - template<> template<> - void llfontgl_object::test<22>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: maxDrawableBytes over text where a character is not a byte", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); // Three characters, three bytes each. const std::string s = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"; const S32 total = (S32)s.size(); - ensure_equals("the fixture string is nine bytes", total, (S32)9); + UNSCOPED_INFO("the fixture string is nine bytes"); + CHECK(total == (S32)9); if (font->getWidthF32Bytes(s, 0, total, false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); - ensure_equals("all the room in the world fits every byte", - font->maxDrawableBytes(s, F32_MAX, total), total); - ensure_equals("no room fits nothing", - font->maxDrawableBytes(s, 0.f, total), (S32)0); + UNSCOPED_INFO("all the room in the world fits every byte"); + CHECK(font->maxDrawableBytes(s, F32_MAX, total) == total); + UNSCOPED_INFO("no room fits nothing"); + CHECK(font->maxDrawableBytes(s, 0.f, total) == (S32)0); // A budget that stops inside a character has to come back to where // that character started. Answering 4 would split it. - ensure_equals("a byte budget inside a character backs off", - font->maxDrawableBytes(s, F32_MAX, 4), (S32)3); - ensure_equals("and again one byte further in", - font->maxDrawableBytes(s, F32_MAX, 5), (S32)3); - ensure_equals("a budget on a boundary is kept", - font->maxDrawableBytes(s, F32_MAX, 6), (S32)6); + UNSCOPED_INFO("a byte budget inside a character backs off"); + CHECK(font->maxDrawableBytes(s, F32_MAX, 4) == (S32)3); + UNSCOPED_INFO("and again one byte further in"); + CHECK(font->maxDrawableBytes(s, F32_MAX, 5) == (S32)3); + UNSCOPED_INFO("a budget on a boundary is kept"); + CHECK(font->maxDrawableBytes(s, F32_MAX, 6) == (S32)6); // By pixels rather than bytes: room for two characters and not three. const F32 w2 = font->getWidthF32Bytes(s, 0, 6, false); const F32 w3 = font->getWidthF32Bytes(s, 0, 9, false); - ensure("the third character has a width", w3 > w2); - ensure_equals("a pixel budget also lands on a character", - font->maxDrawableBytes(s, (w2 + w3) * 0.5f, total), (S32)6); + UNSCOPED_INFO("the third character has a width"); + CHECK(w3 > w2); + UNSCOPED_INFO("a pixel budget also lands on a character"); + CHECK(font->maxDrawableBytes(s, (w2 + w3) * 0.5f, total) == (S32)6); } // begin_offset, and what the answer is counted from. Every one of these // takes an offset to start at, and byteFromPixelOffset reports back // relative to it -- LLLineEditor::calcCursorPos adds its scroll position // to the result, so an absolute answer would double it. - template<> template<> - void llfontgl_object::test<23>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: measuring and hit-testing from begin_offset answer relative to it", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); // "A", a three-byte character, "B" -- and the tail on its own. const std::string s = "A\xE6\x97\xA5" "B"; const std::string tail = "\xE6\x97\xA5" "B"; if (font->getWidthF32Bytes(s, 0, (S32)s.size(), false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); - ensure_equals("measuring from an offset measures the rest", - font->getWidthF32Bytes(s, 1, S32_MAX, false), + UNSCOPED_INFO("measuring from an offset measures the rest"); + CHECK(font->getWidthF32Bytes(s, 1, S32_MAX, false) == font->getWidthF32Bytes(tail, 0, S32_MAX, false)); - ensure_equals("S32_MAX from a non-zero offset does not overflow", - font->getWidthF32Bytes(s, 1, S32_MAX, false), + UNSCOPED_INFO("S32_MAX from a non-zero offset does not overflow"); + CHECK(font->getWidthF32Bytes(s, 1, S32_MAX, false) == font->getWidthF32Bytes(s, 1, (S32)s.size() - 1, false)); - ensure_equals("an offset at the end measures nothing", - font->getWidthF32Bytes(s, (S32)s.size(), S32_MAX, false), 0.f); + UNSCOPED_INFO("an offset at the end measures nothing"); + CHECK(font->getWidthF32Bytes(s, (S32)s.size(), S32_MAX, false) == 0.f); // Hit-testing from an offset answers in the same frame of reference. const F32 w_tail_first = font->getWidthF32Bytes(tail, 0, 3, false); - ensure_equals("hit-testing from an offset is relative to it", - font->byteFromPixelOffset(s, 1, w_tail_first, F32_MAX, - S32_MAX, /*round=*/true), + UNSCOPED_INFO("hit-testing from an offset is relative to it"); + CHECK(font->byteFromPixelOffset(s, 1, w_tail_first, F32_MAX, + S32_MAX, /*round=*/true) == (S32)3); // round=false asks which character the pixel is inside, rather than // which boundary it is nearest, so a point just past a character's // start still belongs to that character. const F32 just_inside = font->getWidthF32Bytes(s, 0, 1, false) + 0.5f; - ensure_equals("round=false stays in the character the pixel is in", - font->byteFromPixelOffset(s, 0, just_inside, F32_MAX, - S32_MAX, /*round=*/false), + UNSCOPED_INFO("round=false stays in the character the pixel is in"); + CHECK(font->byteFromPixelOffset(s, 0, just_inside, F32_MAX, + S32_MAX, /*round=*/false) == (S32)1); } // The two wrap styles that are not ANYWHERE. maxDrawableBytes was rewritten // onto UAX #14 in this work and only its default was ever exercised. - template<> template<> - void llfontgl_object::test<24>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: the word-boundary wrap styles retreat to a break", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "alpha beta"; const S32 total = (S32)s.size(); @@ -864,19 +858,20 @@ namespace // wherever it runs out; the word-boundary styles retreat to the break. const F32 w_partial = font->getWidthF32Bytes(s, 0, 8, false); if (w_partial <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); const S32 anywhere = font->maxDrawableBytes(s, w_partial, total, LLFontGL::ANYWHERE); - ensure("ANYWHERE cuts inside the last word", anywhere > after_space); + UNSCOPED_INFO("ANYWHERE cuts inside the last word"); + CHECK(anywhere > after_space); - ensure_equals("a word boundary is preferred when there is one", - font->maxDrawableBytes(s, w_partial, total, - LLFontGL::WORD_BOUNDARY_IF_POSSIBLE), + UNSCOPED_INFO("a word boundary is preferred when there is one"); + CHECK(font->maxDrawableBytes(s, w_partial, total, + LLFontGL::WORD_BOUNDARY_IF_POSSIBLE) == after_space); - ensure_equals("and required when the style says only", - font->maxDrawableBytes(s, w_partial, total, - LLFontGL::ONLY_WORD_BOUNDARIES), + UNSCOPED_INFO("and required when the style says only"); + CHECK(font->maxDrawableBytes(s, w_partial, total, + LLFontGL::ONLY_WORD_BOUNDARIES) == after_space); // One long word offers no break at all. IF_POSSIBLE has to give the @@ -887,8 +882,8 @@ namespace const S32 no_break = font->maxDrawableBytes(one_word, w_some, (S32)one_word.size(), LLFontGL::WORD_BOUNDARY_IF_POSSIBLE); - ensure("with no legal break the caller still gets progress", - no_break > 0); + UNSCOPED_INFO("with no legal break the caller still gets progress"); + CHECK(no_break > 0); } // maxDrawableBytes shapes a window sized from the pixel budget rather than @@ -902,15 +897,14 @@ namespace // them proves nothing. What proves it is that a small budget over a long // string -- where the window is a fraction of the text -- agrees with the // same question asked over a string short enough that no window applies. - template<> template<> - void llfontgl_object::test<25>() + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: maxDrawableBytes on long text fits what getWidthF32Bytes says fits", "[LLFontGL]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, LLSD(), /*create_gl_textures=*/true); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); // Long enough that a small pixel budget windows well inside it, in a // mix of scripts so the shaping is not trivially uniform. @@ -922,9 +916,10 @@ namespace s += "caf\xC3\xA9 na\xC3\xAFve "; } const S32 total = (S32)s.size(); - ensure("the fixture string is long", total > 1500); + UNSCOPED_INFO("the fixture string is long"); + CHECK(total > 1500); if (font->getWidthF32Bytes(s, 0, 64, false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); // Held against getWidthF32Bytes rather than against maxDrawableBytes // itself. Asking the same function a second question only proves it is @@ -936,19 +931,24 @@ namespace for (F32 px = 8.f; px < 900.f; px += 13.f) { const S32 got = font->maxDrawableBytes(s, px, total, LLFontGL::ANYWHERE); - ensure("the answer stays inside the text", got >= 0 && got <= total); + CAPTURE(px, got); + { + INFO("the answer stays inside the text"); + REQUIRE(got >= 0); + REQUIRE(got <= total); + } if (got > 0) { - ensure("what fits is within the budget", - font->getWidthF32Bytes(s, 0, got, false) <= px); + UNSCOPED_INFO("what fits is within the budget"); + REQUIRE(font->getWidthF32Bytes(s, 0, got, false) <= px); } if (got < total) { // And it is the most that fits: one character more overruns. const S32 more = (S32)utf8str_step_grapheme_forward(s, (size_t)got); - ensure("and one character more does not", - font->getWidthF32Bytes(s, 0, more, false) > px); + UNSCOPED_INFO("and one character more does not"); + REQUIRE(font->getWidthF32Bytes(s, 0, more, false) > px); } } @@ -960,20 +960,24 @@ namespace for (F32 px = 24.f; px < 900.f; px += 37.f) { const S32 got = font->maxDrawableBytes(s, px, total, style); - ensure("a word-boundary answer stays inside the text", - got >= 0 && got <= total); - ensure("a word-boundary answer never overruns", - got == 0 || font->getWidthF32Bytes(s, 0, got, false) <= px); - ensure("and never exceeds what ANYWHERE would take", - got <= font->maxDrawableBytes(s, px, total, LLFontGL::ANYWHERE)); + CAPTURE(style, px, got); + { + INFO("a word-boundary answer stays inside the text"); + REQUIRE(got >= 0); + REQUIRE(got <= total); + } + UNSCOPED_INFO("a word-boundary answer never overruns"); + REQUIRE((got == 0 || font->getWidthF32Bytes(s, 0, got, false) <= px)); + UNSCOPED_INFO("and never exceeds what ANYWHERE would take"); + REQUIRE(got <= font->maxDrawableBytes(s, px, total, LLFontGL::ANYWHERE)); } } // A budget nothing fits in, and a budget everything fits in. - ensure_equals("no room fits nothing", - font->maxDrawableBytes(s, 0.f, total), (S32)0); - ensure_equals("all the room takes everything", - font->maxDrawableBytes(s, F32_MAX, total), total); + UNSCOPED_INFO("no room fits nothing"); + CHECK(font->maxDrawableBytes(s, 0.f, total) == (S32)0); + UNSCOPED_INFO("all the room takes everything"); + CHECK(font->maxDrawableBytes(s, F32_MAX, total) == total); } @@ -985,8 +989,6 @@ namespace // static fontp cache after a few cycles (see vertexbuffer fixture). // =================================================================== -namespace -{ inline void ensureRenderLLFontGL() { static bool initialized = false; @@ -1005,22 +1007,101 @@ namespace ~llfontgl_render_data() = default; }; - typedef test_group llfontgl_render_test; - typedef llfontgl_render_test::object llfontgl_render_object; - tut::llfontgl_render_test llfontgl_render_testcase("LLFontGLRender"); -} + + // What a reflow costs the shaper. LLTextBase wraps by asking + // maxDrawableBytes how much of the remaining text fits, then asking + // getWidthF32Bytes how wide that answer is -- two questions about the same + // run, per line, per segment, and every one of them can be a HarfBuzz + // pass. The shape cache turns the repeats into lookups only when the two + // calls agree on the slice they are asking about. + // + // Counted in cache mutations, which rise once per insert, so the number is + // exactly "how many runs had to be shaped". Timing would not survive a + // build machine; this does. + TEST_CASE_METHOD(llfontgl_data, "LLFontGL: a reflow costs no shapes at the same width and few in a drag", "[LLFontGL]") + { + if (!fileExists(kFontsXml)) + SKIP("fonts.xml not found"); + LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, + LLSD(), /*create_gl_textures=*/true); + LLFontGL* font = LLFontGL::getFontSansSerif(); + REQUIRE(font != nullptr); + + // A chat line long enough to wrap several times at a readable width. + const std::string paragraph = + "The quick brown fox jumps over the lazy dog while the " + "second sentence carries on for long enough to wrap a few " + "times at any sensible chat width at all."; + + // One pass of LLTextBase's wrap loop over one segment, counting the + // shapes it costs. Mirrors LLNormalTextSegment::getNumBytes followed + // by getDimensionsF32. + auto wrap_once = [&](F32 width) -> size_t + { + const size_t before = ALFontShaping::cacheMutationCount(); + S32 at = 0; + while (at < (S32)paragraph.size()) + { + const std::string_view rest = std::string_view(paragraph).substr((size_t)at); + const S32 fits = font->maxDrawableBytes(rest, width, + (S32)rest.size(), + LLFontGL::WORD_BOUNDARY_IF_POSSIBLE); + if (fits <= 0) + { + break; + } + // The width of exactly what fit, which is what the wrap loop + // asks for next to place the line. + (void)font->getWidthF32Bytes(paragraph, at, fits, true); + at += fits; + } + return ALFontShaping::cacheMutationCount() - before; + }; + + // Cold, so every distinct slice counts. + ALFontShaping::clearCache(); + const size_t first = wrap_once(220.f); + UNSCOPED_INFO("wrapping shapes something"); + CHECK(first > 0); + + // The same width again: every slice is already cached, so a repeated + // reflow at an unchanged width must cost nothing. + const size_t again = wrap_once(220.f); + UNSCOPED_INFO("re-wrapping at the same width re-shapes nothing"); + CHECK(again == size_t(0)); + + // A drag-resize. Each frame moves the width a pixel or two, and the + // wrap points move with it, so the slices differ from the ones the + // last frame cached. This is what a resize costs per frame per + // segment, and it is the number worth watching. + size_t dragging = 0; + for (S32 px = 219; px >= 200; --px) + { + dragging += wrap_once((F32)px); + } + + // Not an assertion about a good number -- an assertion that the number + // is bounded by the work there is to do. Twenty widths over a + // paragraph that wraps into a handful of lines: if this ever exceeds + // two shapes per line per width, the two callers have stopped agreeing + // on their slices again. + const size_t lines_per_wrap = first; + std::printf("[reflow shaping] cold=%zu same-width=%zu drag-20px=%zu\n", + first, again, dragging); + UNSCOPED_INFO("the drag stays within two shapes per line per width"); + CHECK(dragging <= 2 * lines_per_wrap * 20); + } // First end-to-end render() call with LEFT/BASELINE places glyph // pixels into the framebuffer at the requested x. Pins the full // chain: shape → atlas → vertex buffer → drawArrays → fragment // output → glReadPixels. - template<> template<> - void llfontgl_render_object::test<1>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes of one character emits one textured quad", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); gl.clearFramebuffer(); const std::string s = "A"; @@ -1042,16 +1123,20 @@ namespace // Force any pending verts to draw. gGL.flush(); gGL.endList(); - ensure_equals("render returned 1 char", n, 1); + CHECK(n == 1); U32 verts = 0; for (const LLVertexBufferData& entry : capture) { verts += entry.mCount; } - ensure_equals("one glyph is one quad", verts, (U32)6); - ensure("the quad carries an atlas texture", - !capture.empty() && capture.front().mTexName != 0); + UNSCOPED_INFO("one glyph is one quad"); + CHECK(verts == (U32)6); + { + INFO("the quad carries an atlas texture"); + REQUIRE_FALSE(capture.empty()); + CHECK(capture.front().mTexName != 0); + } } // HCENTER and RIGHT alignment branches: render() must complete @@ -1060,11 +1145,10 @@ namespace // verification is brittle in this test setup (stub shader / state // accumulation); this pins crash-free traversal of all three // halign branches at llfontgl.cpp:354-372. - template<> template<> - void llfontgl_render_object::test<2>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the horizontal alignment", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "ABC"; @@ -1078,20 +1162,19 @@ namespace LLFontGL::RIGHT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 3); gGL.flush(); - ensure_equals("LEFT render returns 3 bytes", n_left, 3); - ensure_equals("HCENTER render returns 3 bytes", n_cen, 3); - ensure_equals("RIGHT render returns 3 bytes", n_right, 3); + CHECK(n_left == 3); + CHECK(n_cen == 3); + CHECK(n_right == 3); } // VAlign branches: render() must complete through TOP, VCENTER, // BASELINE, and BOTTOM without crashing; return value matches // requested char count for each. Pins the valign switch at // llfonttextcache.cpp:123-138. - template<> template<> - void llfontgl_render_object::test<3>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the vertical alignment", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "Hi"; @@ -1105,19 +1188,18 @@ namespace LLFontGL::LEFT, LLFontGL::BOTTOM, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); gGL.flush(); - ensure_equals("TOP render returns 2 bytes", n_top, 2); - ensure_equals("BASELINE render returns 2 bytes", n_bs, 2); - ensure_equals("BOTTOM render returns 2 bytes", n_btm, 2); + CHECK(n_top == 2); + CHECK(n_bs == 2); + CHECK(n_btm == 2); } // Style flip across renders: render with NORMAL style, then BOLD. // Both must succeed. Pins style propagation through render() — // 00f3a4e93d's regression invalidation gate. - template<> template<> - void llfontgl_render_object::test<4>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the style", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "Hello"; @@ -1131,9 +1213,9 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::UNDERLINE, LLFontGL::NO_SHADOW, 5); gGL.flush(); - ensure_equals("NORMAL render returns 5 bytes", n_norm, 5); - ensure_equals("BOLD render returns 5 bytes", n_bold, 5); - ensure_equals("UNDERLINE render returns 5 bytes", n_und, 5); + CHECK(n_norm == 5); + CHECK(n_bold == 5); + CHECK(n_und == 5); } // Shadow rendering paths: render with NO_SHADOW, DROP_SHADOW, and @@ -1143,11 +1225,10 @@ namespace // to be gUIProgram, which the fixture ensures. Pins shadow // rendering doesn't crash; visible-shadow geometry verification is // out of scope for this fixture (no production shadow shader). - template<> template<> - void llfontgl_render_object::test<5>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes draws every byte whatever the shadow", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); const std::string s = "Hi"; @@ -1161,9 +1242,9 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::DROP_SHADOW_SOFT, 2); gGL.flush(); - ensure_equals("NO_SHADOW returns 2", n_no, 2); - ensure_equals("DROP_SHADOW returns 2", n_drop, 2); - ensure_equals("DROP_SHADOW_SOFT returns 2", n_soft, 2); + CHECK(n_no == 2); + CHECK(n_drop == 2); + CHECK(n_soft == 2); } // Mid-render glyph rasterization must not misdirect the pending @@ -1181,15 +1262,14 @@ namespace // directly observable: render a Latin prefix plus a never-yet- // rasterized emoji and require the prefix batch to carry the // grayscale atlas texture its UVs were built against. - template<> template<> - void llfontgl_render_object::test<6>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: mid-render glyph rasterization keeps the pending batch's texture", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const LLFontFreetype* head = font->getFontFreetype(); - ensure("head freetype resolves", head != nullptr); + REQUIRE(head != nullptr); // U+1F995 SAUROPOD — obscure enough that no other test in this // binary rasterizes it. The mid-render upload only fires on a @@ -1198,11 +1278,10 @@ namespace U32 emoji_idx = 0; const LLFontFreetype* emoji_face = head->selectShapingFace(emoji_cp, emoji_idx); if (!emoji_face || emoji_face == head || emoji_idx == 0) - skip("no emoji fallback covers U+1F995 in this harness"); - ensure("emoji fallback has a face wrapper", - emoji_face->getFontFace() != nullptr); + SKIP("no emoji fallback covers U+1F995 in this harness"); + REQUIRE(emoji_face->getFontFace() != nullptr); if (emoji_face->getFontFace()->findGlyphInfo(emoji_idx, EFontGlyphType::Color)) - skip("U+1F995 already rasterized; mid-render upload won't fire"); + SKIP("U+1F995 already rasterized; mid-render upload won't fire"); const std::string s = "stomp \xF0\x9F\xA6\x95 check"; @@ -1213,42 +1292,54 @@ namespace LLFontGL::NORMAL, LLFontGL::NO_SHADOW, (S32)s.size()); gGL.endList(); - ensure_equals("render consumed the whole string", n, (S32)s.size()); + UNSCOPED_INFO("render consumed the whole string"); + REQUIRE(n == (S32)s.size()); // Expected textures, resolved through the glyph entries the render // itself produced. use_color defaulted true, so the render looked // glyphs up as Color; querying the same way returns the same // entries (and therefore the same atlas pages) it drew from. const LLFontGlyphInfo* latin_gi = head->getGlyphInfo((llwchar)'s', EFontGlyphType::Color); - ensure("latin glyph cached", latin_gi && latin_gi->mSourceFace); + { + INFO("latin glyph cached"); + REQUIRE(latin_gi); + REQUIRE(latin_gi->mSourceFace); + } const auto& latin_entry = latin_gi->mPhaseSlots[0].mBitmapEntry; LLImageGL* latin_img = latin_gi->mSourceFace->getBitmapCache()->getImageGL( latin_entry.first, (U32)latin_entry.second); - ensure("latin atlas page live", latin_img != nullptr); + UNSCOPED_INFO("latin atlas page live"); + REQUIRE(latin_img != nullptr); const U32 latin_tex = latin_img->getTexName(); const LLFontGlyphInfo* emoji_gi = head->getGlyphInfo(emoji_cp, EFontGlyphType::Color); - ensure("emoji glyph cached", emoji_gi && emoji_gi->mSourceFace); + { + INFO("emoji glyph cached"); + REQUIRE(emoji_gi); + REQUIRE(emoji_gi->mSourceFace); + } const auto& emoji_entry = emoji_gi->mPhaseSlots[0].mBitmapEntry; LLImageGL* emoji_img = emoji_gi->mSourceFace->getBitmapCache()->getImageGL( emoji_entry.first, (U32)emoji_entry.second); - ensure("emoji atlas page live", emoji_img != nullptr); + UNSCOPED_INFO("emoji atlas page live"); + REQUIRE(emoji_img != nullptr); const U32 emoji_tex = emoji_img->getTexName(); - ensure("string spans two distinct atlas textures", latin_tex != emoji_tex); - ensure("capture produced batches", !capture.empty()); + UNSCOPED_INFO("string spans two distinct atlas textures"); + CHECK(latin_tex != emoji_tex); + REQUIRE_FALSE(capture.empty()); // The first captured batch is the Latin prefix, flushed when the // glyph stream switches to the emoji's atlas — exactly the batch // the mid-render upload used to misdirect. - ensure_equals("prefix batch carries its own atlas texture", - capture.front().mTexName, latin_tex); + UNSCOPED_INFO("prefix batch carries its own atlas texture"); + CHECK(capture.front().mTexName == latin_tex); // And every batch in the capture must reference one of the two // atlas pages this string actually draws from. for (const LLVertexBufferData& entry : capture) { - ensure("batch texture is one of the string's atlas pages", - entry.mTexName == latin_tex || entry.mTexName == emoji_tex); + UNSCOPED_INFO("batch texture is one of the string's atlas pages"); + REQUIRE((entry.mTexName == latin_tex || entry.mTexName == emoji_tex)); } } @@ -1256,26 +1347,27 @@ namespace // a codepoint count, a glyph count or a cluster count, and every existing // assertion about it is over ASCII. Three characters of three bytes each // tell the four apart: nine, and nothing else. - template<> template<> - void llfontgl_render_object::test<7>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes counts bytes rather than characters", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"; const S32 total = (S32)s.size(); - ensure_equals("the fixture string is nine bytes", total, (S32)9); + UNSCOPED_INFO("the fixture string is nine bytes"); + CHECK(total == (S32)9); if (font->getWidthF32Bytes(s, 0, total, false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); const S32 drawn = font->renderBytes(s, 0, 100.f, 100.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, total); gGL.flush(); - ensure_equals("renderBytes returns bytes, not characters", drawn, total); + UNSCOPED_INFO("renderBytes returns bytes, not characters"); + CHECK(drawn == total); // A budget of one character is three bytes, and drawing from an offset // reports what it drew rather than where it stopped. @@ -1284,15 +1376,16 @@ namespace LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 3); gGL.flush(); - ensure_equals("a one-character budget is three bytes", one, (S32)3); + UNSCOPED_INFO("a one-character budget is three bytes"); + CHECK(one == (S32)3); const S32 tail = font->renderBytes(s, 3, 100.f, 140.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX); gGL.flush(); - ensure_equals("drawing from an offset returns the bytes it drew", - tail, total - 3); + UNSCOPED_INFO("drawing from an offset returns the bytes it drew"); + CHECK(tail == total - 3); } // Clipping is per cluster, and the count says so. Several glyphs can carry @@ -1301,15 +1394,14 @@ namespace // a per-glyph clip can paint a base and drop the mark that belongs to it. // That is not a clipped syllable, it is a different syllable, and the // count reported back calls the whole cluster undrawn either way. - template<> template<> - void llfontgl_render_object::test<8>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes clips whole clusters and counts what it drew", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("freetype present", ft != nullptr); + REQUIRE(ft != nullptr); // Devanagari, which shapes two glyphs to a cluster with an advance on // each -- the shape that a per-glyph clip splits. @@ -1318,13 +1410,13 @@ namespace const S32 total = (S32)s.size(); const F32 full = font->getWidthF32Bytes(s, 0, total, false); if (full <= 0.f) - skip("no Devanagari coverage in the test font set"); + SKIP("no Devanagari coverage in the test font set"); // Cluster boundaries from the shaper itself rather than by hand: which // bytes are boundaries is the font's business, not this test's. const auto& glyphs = ALFontShaping::shapeLine(ft, s, 0, (size_t)total); if (glyphs.empty()) - skip("Devanagari did not shape"); + SKIP("Devanagari did not shape"); std::set boundaries; for (const ALShapedGlyph& g : glyphs) { @@ -1346,6 +1438,7 @@ namespace S32_MAX, (S32)px); gGL.flush(); gGL.endList(); + CAPTURE(px, n); U32 verts = 0; for (const LLVertexBufferData& entry : capture) @@ -1353,8 +1446,8 @@ namespace verts += entry.mCount; } - ensure("a clipped draw stops on a cluster boundary", - boundaries.count(n) == 1); + UNSCOPED_INFO("a clipped draw stops on a cluster boundary"); + REQUIRE(boundaries.count(n) == 1); // Exactly the glyphs of the clusters the count claims. A per-glyph // clip emits the glyphs of the overflowing cluster that came before @@ -1374,11 +1467,14 @@ namespace // its own start_x while getWidthF32Bytes divides the scale back // out. Testing the drawn extent wants the vertex positions out of // the capture, not a re-measurement. - ensure_equals("quads drawn match the bytes reported", verts, expected); + UNSCOPED_INFO("quads drawn match the bytes reported"); + REQUIRE(verts == expected); if (n > 0 && n < total) ++checked; } - ensure("the sweep reached a partially-clipped draw", checked > 0); + UNSCOPED_INFO("the sweep reached a partially-clipped draw"); + CHECK(checked > 0); } + // renderBytes places a right-aligned or centred string by measuring it, and // it takes that measurement from the glyphs it is about to draw rather than // shaping the text a second time. The two have to agree exactly: the number @@ -1389,13 +1485,12 @@ namespace // right_x is the observable end of the drawn run, so LEFT-aligned it is the // origin plus the width. That is what pins the shared sum against the // independent walk. - template<> template<> - void llfontgl_render_object::test<9>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderBytes ends a run exactly where getWidthF32Bytes measures it", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string samples[] = { std::string("Hello world"), @@ -1408,6 +1503,7 @@ namespace for (const std::string& s : samples) { + CAPTURE(s); const S32 total = (S32)s.size(); // no_padding: right_x is where the pen ended, so it carries the // advances and not the extent overhang of the last glyph, which is @@ -1424,92 +1520,9 @@ namespace gGL.flush(); // right_x is in the same unscaled space the origin was given in. - ensure_equals("the drawn run ends where the measurement says", - right_x, 40.f + expect); - } - } - - // What a reflow costs the shaper. LLTextBase wraps by asking - // maxDrawableBytes how much of the remaining text fits, then asking - // getWidthF32Bytes how wide that answer is -- two questions about the same - // run, per line, per segment, and every one of them can be a HarfBuzz - // pass. The shape cache turns the repeats into lookups only when the two - // calls agree on the slice they are asking about. - // - // Counted in cache mutations, which rise once per insert, so the number is - // exactly "how many runs had to be shaped". Timing would not survive a - // build machine; this does. - template<> template<> - void llfontgl_object::test<26>() - { - if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); - LLFontGL::initClass(96.f, 1.f, 1.f, kAppDir, kFontsXml, - LLSD(), /*create_gl_textures=*/true); - LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); - - // A chat line long enough to wrap several times at a readable width. - const std::string paragraph = - "The quick brown fox jumps over the lazy dog while the " - "second sentence carries on for long enough to wrap a few " - "times at any sensible chat width at all."; - - // One pass of LLTextBase's wrap loop over one segment, counting the - // shapes it costs. Mirrors LLNormalTextSegment::getNumBytes followed - // by getDimensionsF32. - auto wrap_once = [&](F32 width) -> size_t - { - const size_t before = ALFontShaping::cacheMutationCount(); - S32 at = 0; - while (at < (S32)paragraph.size()) - { - const std::string_view rest = std::string_view(paragraph).substr((size_t)at); - const S32 fits = font->maxDrawableBytes(rest, width, - (S32)rest.size(), - LLFontGL::WORD_BOUNDARY_IF_POSSIBLE); - if (fits <= 0) - { - break; - } - // The width of exactly what fit, which is what the wrap loop - // asks for next to place the line. - (void)font->getWidthF32Bytes(paragraph, at, fits, true); - at += fits; - } - return ALFontShaping::cacheMutationCount() - before; - }; - - // Cold, so every distinct slice counts. - ALFontShaping::clearCache(); - const size_t first = wrap_once(220.f); - ensure("wrapping shapes something", first > 0); - - // The same width again: every slice is already cached, so a repeated - // reflow at an unchanged width must cost nothing. - const size_t again = wrap_once(220.f); - ensure_equals("re-wrapping at the same width re-shapes nothing", again, size_t(0)); - - // A drag-resize. Each frame moves the width a pixel or two, and the - // wrap points move with it, so the slices differ from the ones the - // last frame cached. This is what a resize costs per frame per - // segment, and it is the number worth watching. - size_t dragging = 0; - for (S32 px = 219; px >= 200; --px) - { - dragging += wrap_once((F32)px); + UNSCOPED_INFO("the drawn run ends where the measurement says"); + REQUIRE(right_x == 40.f + expect); } - - // Not an assertion about a good number -- an assertion that the number - // is bounded by the work there is to do. Twenty widths over a - // paragraph that wraps into a handful of lines: if this ever exceeds - // two shapes per line per width, the two callers have stopped agreeing - // on their slices again. - const size_t lines_per_wrap = first; - std::printf("[reflow shaping] cold=%zu same-width=%zu drag-20px=%zu\n", - first, again, dragging); - ensure("the drag stays within two shapes per line per width", - dragging <= 2 * lines_per_wrap * 20); } // An ellipsized draw buys itself padding by measuring "....", and that @@ -1520,13 +1533,12 @@ namespace // to be read after the dots are measured. Read before, the first // ellipsized string of a session trips a tripwire for a walk that has not // started yet, which on the login screen is every session. - template<> template<> - void llfontgl_render_object::test<10>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: an ellipsized draw on a cold cache shapes the dots mid-draw", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "a string rather wider than the budget it is given"; @@ -1534,7 +1546,8 @@ namespace // draw below is the ellipsis measurement. ALFontShaping::clearCache(); const F32 full = font->getWidthF32(s); - ensure("the sample string has width", full > 0.f); + UNSCOPED_INFO("the sample string has width"); + CHECK(full > 0.f); const size_t before = ALFontShaping::cacheMutationCount(); @@ -1549,22 +1562,21 @@ namespace gGL.flush(); gGL.endList(); - ensure("an ellipsized draw stops short of the whole string", - n < (S32)s.size()); + UNSCOPED_INFO("an ellipsized draw stops short of the whole string"); + REQUIRE(n < (S32)s.size()); // Without this the test proves nothing: it is the dots landing in the // cache mid-draw that puts the snapshot and the count out of step. - ensure("the ellipsis measurement shaped inside the draw", - ALFontShaping::cacheMutationCount() > before); + UNSCOPED_INFO("the ellipsis measurement shaped inside the draw"); + CHECK(ALFontShaping::cacheMutationCount() > before); } // A string placed by placeGlyphs and drawn by renderGlyphs lands on the // pixels renderUTF8 puts it on, whichever way it is aligned; and runs // drawn in one call by renderGlyphRuns are the runs drawn apiece. - template<> template<> - void llfontgl_render_object::test<11>() + TEST_CASE_METHOD(llfontgl_render_data, "LLFontGLRender: renderGlyphs matches renderUTF8 and renderGlyphRuns matches renderGlyphs", "[LLFontGLRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not found"); + SKIP("fonts.xml not found"); const S32 w = ll_test::HeadlessGL::WIDTH; const S32 h = ll_test::HeadlessGL::HEIGHT; const auto frame = [&](const std::function& draw) { @@ -1588,12 +1600,12 @@ namespace const LLFontGL::HAlign aligns[] = { LLFontGL::LEFT, LLFontGL::RIGHT, LLFontGL::HCENTER }; for (LLFontGL* font : { LLFontGL::getFontMonospace(), LLFontGL::getFontSansSerif() }) { - ensure("font resolves", font != nullptr); + REQUIRE(font != nullptr); for (const std::string s : { "7", "42", "12345", "Hello, world" }) { for (LLFontGL::HAlign halign : aligns) { - const std::string what = s + " aligned " + std::to_string((int)halign); + CAPTURE(s, halign); const std::vector said = frame([&] { font->renderUTF8(s, 0, 128.f, 100.f, LLColor4::white, halign, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW); }); @@ -1601,8 +1613,10 @@ namespace const F32 shift = font->placeGlyphs(s, halign, placed); const std::vector colors(placed.size(), LLColor4U(255, 255, 255, 255)); const std::vector drawn = frame([&] { font->renderGlyphs(placed.data(), colors.data(), placed.size(), 128.f + shift, 100.f); }); - ensure(what + ": drawn at all", lit(said)); - ensure(what + ": the same pixels", said == drawn); + UNSCOPED_INFO("drawn at all"); + REQUIRE(lit(said)); + UNSCOPED_INFO("the same pixels"); + REQUIRE(said == drawn); } } } @@ -1620,7 +1634,9 @@ namespace const LLFontGL::GlyphRun runs[] = { { first.data(), red.data(), first.size(), 20.f, 50.f }, { second.data(), green.data(), second.size(), 100.f, 150.f } }; const std::vector together = frame([&] { font->renderGlyphRuns(runs, 2); }); - ensure("runs drawn at all", lit(apiece)); - ensure("runs in one call are the runs apiece", apiece == together); + UNSCOPED_INFO("runs drawn at all"); + CHECK(lit(apiece)); + UNSCOPED_INFO("runs in one call are the runs apiece"); + CHECK(apiece == together); } } diff --git a/indra/llrender/tests/llfontregistry_gl_test.cpp b/indra/llrender/tests/llfontregistry_gl_test.cpp index 700ace4bbb..7e96537ebe 100644 --- a/indra/llrender/tests/llfontregistry_gl_test.cpp +++ b/indra/llrender/tests/llfontregistry_gl_test.cpp @@ -37,7 +37,7 @@ #include "llfonttest_helpers.h" #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include @@ -151,65 +151,53 @@ namespace tut } }; - typedef test_group llfontregistry_gl_test; - typedef llfontregistry_gl_test::object llfontregistry_gl_object; -namespace -{ - tut::llfontregistry_gl_test llfontregistry_gl_testcase("LLFontRegistry-GL"); -} - // createFont with mCreateGLTextures=true must produce a head whose // bitmap cache, after rasterizing the ASCII range, has at least one // grayscale page bound to a live GL texture. - template<> template<> - void llfontregistry_gl_object::test<1>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: createFont with GL textures backs the ASCII atlas with a live texture", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves Inter/Small", font != nullptr); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontFreetype* ft = font->getFontFreetype(); - ensure("LLFontFreetype present", ft != nullptr); + REQUIRE(ft != nullptr); const LLFontBitmapCache* cache = ft->getFontBitmapCache(); - ensure("bitmap cache present", cache != nullptr); - ensure("at least one grayscale atlas page", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + REQUIRE(cache != nullptr); + CHECK(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); LLImageGL* page = cache->getImageGL(EFontGlyphType::Grayscale, 0); - ensure("page 0 LLImageGL present", page != nullptr); - ensure("page 0 has a live GL texture name", - page->getTexName() != 0); + REQUIRE(page != nullptr); + CHECK(page->getTexName() != 0); } // getFont(desc) must return the same LLFontGL pointer on a repeat // call — the registry caches by descriptor and a second hit must // not re-create or re-rasterize the head. - template<> template<> - void llfontregistry_gl_object::test<2>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: getFont returns the cached LLFontGL on a repeat call", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + CHECK(loadXml(kInterXml)); LLFontGL* first = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("first getFont resolves", first != nullptr); + REQUIRE(first != nullptr); LLFontGL* second = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("second getFont resolves", second != nullptr); - ensure("repeat getFont returns the same LLFontGL pointer", - first == second); + REQUIRE(second != nullptr); + CHECK(first == second); } // destroyGL must drop GL textures and reset the face's bitmap cache @@ -219,33 +207,28 @@ namespace // the LLImageGL vector entirely, so post-destroyGL the page count // is 0 (not "page count preserved with texname=0"). The next render // through the face re-allocates fresh atlas pages. - template<> template<> - void llfontregistry_gl_object::test<3>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: destroyGL empties the atlas but keeps the registry's LLFontGL", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves", font != nullptr); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontBitmapCache* cache = font->getFontFreetype()->getFontBitmapCache(); - ensure("at least one atlas page before destroyGL", - cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); - ensure("texname live before destroyGL", - cache->getImageGL(EFontGlyphType::Grayscale, 0)->getTexName() != 0); + REQUIRE(cache->getNumBitmaps(EFontGlyphType::Grayscale) >= 1u); + CHECK(cache->getImageGL(EFontGlyphType::Grayscale, 0)->getTexName() != 0); reg.destroyGL(); - ensure_equals("page vector cleared after destroyGL", - (S32)cache->getNumBitmaps(EFontGlyphType::Grayscale), 0); - ensure("registry entry survived destroyGL — same LLFontGL pointer", - reg.getFont(LLFontDescriptor("Inter", "Small", 0)) == font); + CHECK((S32)cache->getNumBitmaps(EFontGlyphType::Grayscale) == 0); + CHECK(reg.getFont(LLFontDescriptor("Inter", "Small", 0)) == font); } // End-to-end runtime size-change reload. Verify that wiping registry @@ -254,12 +237,11 @@ namespace // size. Indirect because LLFontFreetype::mPointSize is private — // ascender height is FreeType metrics * pointSize/units_per_EM, so // doubling the point size doubles the ascender (within rounding). - template<> template<> - void llfontregistry_gl_object::test<4>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: a reload at twice the point size roughly doubles the ascender", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } @@ -271,11 +253,11 @@ namespace " " " " ""; - ensure("v1 parse ok", loadXml(kV1)); + CHECK(loadXml(kV1)); LLFontGL* font_v1 = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("v1 getFont resolves", font_v1 != nullptr); + REQUIRE(font_v1 != nullptr); F32 ascender_v1 = font_v1->getAscenderHeight(); - ensure("v1 ascender > 0", ascender_v1 > 0.f); + CHECK(ascender_v1 > 0.f); // Wipe + re-parse with 2x size. simulateReload(); @@ -286,16 +268,15 @@ namespace " " " " ""; - ensure("v2 parse ok", loadXml(kV2)); + CHECK(loadXml(kV2)); LLFontGL* font_v2 = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("v2 getFont resolves", font_v2 != nullptr); + REQUIRE(font_v2 != nullptr); F32 ascender_v2 = font_v2->getAscenderHeight(); - ensure("v2 ascender > 0", ascender_v2 > 0.f); + CHECK(ascender_v2 > 0.f); // Doubling pt should ~double ascender (allow generous tolerance for // FreeType rounding: 1.7x lower bound is comfortably above 1.0x). - ensure("ascender scales with reloaded point size", - ascender_v2 > ascender_v1 * 1.7f); + CHECK(ascender_v2 > ascender_v1 * 1.7f); } // End-to-end override -> clear -> override-again with DIFFERENT font @@ -306,14 +287,13 @@ namespace // verify the head's freetype actually changed AND the point size moved. // Pins both halves of the loop: file swap on override change, AND // size routing through the head-face chain. - template<> template<> - void llfontregistry_gl_object::test<5>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: clearing an override reloads the head with another file and size", "[LLFontRegistry-GL]") { const std::string od_path = std::string(kFontDir) + "OpenDyslexic-Regular.otf"; const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(od_path) || !fileExists(inter_path)) { - skip("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); + SKIP("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); return; } @@ -333,25 +313,25 @@ namespace ""; // First load: override SansSerifBase -> OpenDyslexic. - ensure("parse v1 ok", loadXml(kXml)); + CHECK(loadXml(kXml)); LLSD overrides; overrides["SansSerifBase"] = "OpenDyslexic"; resolve(overrides); LLFontGL* font_with = reg.getFont(LLFontDescriptor("SansSerif", "Large", 0)); - ensure("getFont with override resolves", font_with != nullptr); + REQUIRE(font_with != nullptr); F32 ascender_with = font_with->getAscenderHeight(); - ensure("ascender with override > 0", ascender_with > 0.f); + CHECK(ascender_with > 0.f); // Wipe + re-parse with no override. simulateReload(); - ensure("parse v2 ok", loadXml(kXml)); + CHECK(loadXml(kXml)); resolve(LLSD()); LLFontGL* font_no = reg.getFont(LLFontDescriptor("SansSerif", "Large", 0)); - ensure("getFont without override resolves", font_no != nullptr); + REQUIRE(font_no != nullptr); F32 ascender_no = font_no->getAscenderHeight(); - ensure("ascender without override > 0", ascender_no > 0.f); + CHECK(ascender_no > 0.f); // The two states must produce visibly different ascender heights: // With override: OpenDyslexic at 14pt — large intrinsic + larger pt. @@ -362,8 +342,7 @@ namespace const F32 diff = (ascender_with > ascender_no) ? (ascender_with - ascender_no) : (ascender_no - ascender_with); - ensure("override and no-override produce distinguishable ascenders", - diff > ascender_no * 0.1f); + CHECK(diff > ascender_no * 0.1f); } // Per-family on a fallback family pins THAT family's files at @@ -373,14 +352,13 @@ namespace // and find the DejaVu fallback — its getAscenderHeight() reflects // 8pt rendering, not 16pt. Every is an absolute pin (no // force keyword needed). - template<> template<> - void llfontregistry_gl_object::test<6>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: a fallback family's pins its files below the head's size", "[LLFontRegistry-GL]") { const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; const std::string dejavu_path = std::string(kFontDir) + "DejaVuSans.woff2"; if (!fileExists(inter_path) || !fileExists(dejavu_path)) { - skip("InterVariable.woff2 or DejaVuSans.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 or DejaVuSans.woff2 not present in test data dir"); return; } @@ -397,14 +375,14 @@ namespace " " ""; - ensure("parse ok", loadXml(kXml)); + CHECK(loadXml(kXml)); resolve(); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Large", 0)); - ensure("getFont resolves", font != nullptr); + REQUIRE(font != nullptr); // Head is Inter at chain's 16pt. F32 head_ascender = font->getAscenderHeight(); - ensure("head ascender > 0", head_ascender > 0.f); + CHECK(head_ascender > 0.f); // Find the DejaVu fallback in the chain. Its mPointSize should be // the per-family pin 8.0, NOT the chain's 16.0. @@ -418,16 +396,15 @@ namespace break; } } - ensure("DejaVu fallback present in chain", dejavu_fallback != nullptr); + REQUIRE(dejavu_fallback != nullptr); F32 fb_ascender = dejavu_fallback->getAscenderHeight(); - ensure("DejaVu fallback ascender > 0", fb_ascender > 0.f); + CHECK(fb_ascender > 0.f); // The pinned fallback at 8pt must be substantially smaller than // the head's 16pt rendering. Demand >40% smaller — comfortably // separates "pin applied" from "fell through to chain pt". - ensure("pinned fallback ascender is substantially smaller than head", - fb_ascender < head_ascender * 0.6f); + CHECK(fb_ascender < head_ascender * 0.6f); } // End-to-end override SWAP between two distinct pinned sources, with @@ -438,14 +415,13 @@ namespace // back-to-back override swap, with no leftover state from the first // source. Mirrors the picker workflow of moving between two custom // UI fonts that each declare their own preferred size. - template<> template<> - void llfontregistry_gl_object::test<7>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: an override swap between pinned sources reloads at the new pin", "[LLFontRegistry-GL]") { const std::string od_path = std::string(kFontDir) + "OpenDyslexic-Regular.otf"; const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(od_path) || !fileExists(inter_path)) { - skip("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); + SKIP("OpenDyslexic-Regular.otf or InterVariable.woff2 not present in test data dir"); return; } @@ -466,32 +442,32 @@ namespace ""; // Override A: Target -> OpenDyslexic. Per-file pin lands head at 14pt. - ensure("v1 parse ok", loadXml(kXml)); + CHECK(loadXml(kXml)); LLSD ovr_a; ovr_a["Target"] = "OpenDyslexic"; resolve(ovr_a); LLFontGL* font_a = reg.getFont(LLFontDescriptor("Target", "Large", 0)); - ensure("getFont under OpenDyslexic override", font_a != nullptr); + REQUIRE(font_a != nullptr); F32 asc_a = font_a->getAscenderHeight(); - ensure("OpenDyslexic ascender > 0", asc_a > 0.f); + CHECK(asc_a > 0.f); // Swap: Target -> Inter (pin Large=8). Head must reload at 8pt // with Inter's intrinsics — substantially smaller than 14pt OD. simulateReload(); - ensure("v2 parse ok", loadXml(kXml)); + CHECK(loadXml(kXml)); LLSD ovr_b; ovr_b["Target"] = "Inter"; resolve(ovr_b); LLFontGL* font_b = reg.getFont(LLFontDescriptor("Target", "Large", 0)); - ensure("getFont under Inter override", font_b != nullptr); + REQUIRE(font_b != nullptr); F32 asc_b = font_b->getAscenderHeight(); - ensure("Inter ascender > 0", asc_b > 0.f); + CHECK(asc_b > 0.f); // Inter at 8pt is comfortably smaller than OpenDyslexic at 14pt. // 60% threshold separates "pin swapped cleanly" from "stuck at // OD's 14pt" with margin against FreeType rounding noise. - ensure("ascender shrinks across override swap (OD 14pt -> Inter 8pt)", - asc_b < asc_a * 0.6f); + UNSCOPED_INFO("ascender shrinks across override swap (OD 14pt -> Inter 8pt)"); + CHECK(asc_b < asc_a * 0.6f); } // F-NULL1 regression: getFont for a descriptor whose font files all @@ -500,8 +476,7 @@ namespace // the next getFont silently returned the cached NULL and widgets // dereferenced it; the fix at llfontregistry.cpp:1555-1567 only // inserts on success. This test pins that contract. - template<> template<> - void llfontregistry_gl_object::test<8>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: a font whose files all fail to load is not cached as NULL", "[LLFontRegistry-GL]") { // No file presence check needed — the whole point is that the // file is missing. @@ -512,15 +487,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(kXml)); + CHECK(loadXml(kXml)); resolve(); LLFontDescriptor desc("Phantom", "Small", 0); LLFontGL* first = reg.getFont(desc); - ensure("getFont with missing file returns NULL", first == nullptr); + CHECK(first == nullptr); - ensure("failed createFont must NOT poison mFontMap with a NULL slot", - !fontMapContains(desc)); + CHECK_FALSE(fontMapContains(desc)); } // reloadForDpiChange walks heads + fallback cache and resets each @@ -529,31 +503,29 @@ namespace // (FreeType point→pixel conversion goes through DPI). Pins the // fast-DPI path that LLFontGL::initClass takes when fonts.xml + // overrides are unchanged but only the scale moved. - template<> template<> - void llfontregistry_gl_object::test<9>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: reloadForDpiChange at twice the DPI roughly doubles the ascender", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves", font != nullptr); + REQUIRE(font != nullptr); F32 ascender_96 = font->getAscenderHeight(); - ensure("ascender at 96 DPI > 0", ascender_96 > 0.f); + CHECK(ascender_96 > 0.f); LLFontGL::sVertDPI = 192.f; LLFontGL::sHorizDPI = 192.f; reg.reloadForDpiChange(); F32 ascender_192 = font->getAscenderHeight(); - ensure("ascender at 192 DPI > 0", ascender_192 > 0.f); + CHECK(ascender_192 > 0.f); // 192/96 = 2.0x; allow 1.7x lower bound for FreeType rounding. - ensure("ascender scales with DPI after reloadForDpiChange", - ascender_192 > ascender_96 * 1.7f); + CHECK(ascender_192 > ascender_96 * 1.7f); // Restore so neighbour tests see the fixture default. LLFontGL::sVertDPI = 96.f; @@ -566,29 +538,27 @@ namespace // must not drop in-use atlas pages on a short-interval invocation. // Pin: after generating ASCII glyphs, a sweep doesn't reduce page // count. - template<> template<> - void llfontregistry_gl_object::test<10>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: sweepGlyphCaches keeps in-use atlas pages", "[LLFontRegistry-GL]") { if (!interFontPresent()) { - skip("InterVariable.woff2 not present in test data dir"); + SKIP("InterVariable.woff2 not present in test data dir"); return; } - ensure("parse ok", loadXml(kInterXml)); + CHECK(loadXml(kInterXml)); LLFontGL* font = reg.getFont(LLFontDescriptor("Inter", "Small", 0)); - ensure("getFont resolves", font != nullptr); + REQUIRE(font != nullptr); font->generateASCIIglyphs(); const LLFontBitmapCache* cache = font->getFontFreetype()->getFontBitmapCache(); const U32 pages_before = cache->getNumBitmaps(EFontGlyphType::Grayscale); - ensure("at least one atlas page before sweep", pages_before >= 1u); + CHECK(pages_before >= 1u); reg.sweepGlyphCaches(); const U32 pages_after = cache->getNumBitmaps(EFontGlyphType::Grayscale); - ensure_equals("sweep does not drop in-use atlas pages", - pages_after, pages_before); + CHECK(pages_after == pages_before); } // End-to-end shared-fallback eviction: two heads built via the @@ -612,15 +582,14 @@ namespace // Post-fix: heads have no mGlyphInfoMap; lookup goes through the // face's findGlyphInfo, which misses post-eviction and falls through // to addShapedGlyphFromFont to re-rasterize on a fresh sheet. - template<> template<> - void llfontregistry_gl_object::test<11>() + TEST_CASE_METHOD(llfontregistry_gl_data, "LLFontRegistry-GL: heads sharing an evicted fallback re-rasterize onto a live sheet", "[LLFontRegistry-GL]") { const std::string emoji_path = std::string(kFontDir) + "Noto-COLRv1.ttf"; const std::string dejavu_path = std::string(kFontDir) + "DejaVuSans.woff2"; const std::string inter_path = std::string(kFontDir) + "InterVariable.woff2"; if (!fileExists(emoji_path) || !fileExists(dejavu_path) || !fileExists(inter_path)) { - skip("Noto-COLRv1.ttf + DejaVuSans.woff2 + InterVariable.woff2 required"); + SKIP("Noto-COLRv1.ttf + DejaVuSans.woff2 + InterVariable.woff2 required"); return; } @@ -642,13 +611,13 @@ namespace " " " " ""; - ensure("parse ok", loadXml(kXml)); + CHECK(loadXml(kXml)); resolve(); LLFontGL* head_a = reg.getFont(LLFontDescriptor("HeadA", "Small", 0)); LLFontGL* head_b = reg.getFont(LLFontDescriptor("HeadB", "Small", 0)); - ensure("HeadA resolves", head_a != nullptr); - ensure("HeadB resolves", head_b != nullptr); + REQUIRE(head_a != nullptr); + REQUIRE(head_b != nullptr); // Locate the Noto-COLRv1 fallback inside each head's chain. auto find_emoji_fallback = [](LLFontGL* head) -> const LLFontFreetype* @@ -662,11 +631,11 @@ namespace }; const LLFontFreetype* emoji_a = find_emoji_fallback(head_a); const LLFontFreetype* emoji_b = find_emoji_fallback(head_b); - ensure("HeadA chain includes Noto-COLRv1", emoji_a != nullptr); - ensure("HeadB chain includes Noto-COLRv1", emoji_b != nullptr); - ensure_equals("siblings share the fallback freetype instance " - "(mFallbackInstanceCache dedup)", - emoji_a, emoji_b); + REQUIRE(emoji_a != nullptr); + REQUIRE(emoji_b != nullptr); + UNSCOPED_INFO("siblings share the fallback freetype instance " + "(mFallbackInstanceCache dedup)"); + REQUIRE(emoji_a == emoji_b); // Rasterize an emoji glyph through both heads. Both routes go // through getGlyphInfo → cmap miss on the head face → fallback @@ -677,15 +646,18 @@ namespace kFire, EFontGlyphType::Color); const LLFontGlyphInfo* gi_b = head_b->getFontFreetype()->getGlyphInfo( kFire, EFontGlyphType::Color); - ensure("HeadA produced an emoji glyph entry", gi_a != nullptr); - ensure("HeadB produced an emoji glyph entry", gi_b != nullptr); + REQUIRE(gi_a != nullptr); + REQUIRE(gi_b != nullptr); // Face-level dedup → both heads see the same face-owned entry. - ensure_equals("face dedup returns the same entry to both heads", - gi_a, gi_b); + UNSCOPED_INFO("face dedup returns the same entry to both heads"); + REQUIRE(gi_a == gi_b); const auto target = gi_a->mPhaseSlots[0].mBitmapEntry; - ensure("emoji glyph references a real Color sheet", - target.first == EFontGlyphType::Color && target.second >= 0); + { + INFO("emoji glyph references a real Color sheet"); + CHECK(target.first == EFontGlyphType::Color); + CHECK(target.second >= 0); + } // Simulate the fallback's collectGarbage releasing this sheet. // Delete face-owned entries pointing at (Color, target.second), @@ -694,8 +666,8 @@ namespace // to the deleted entry; post-fix, neither head has such a cache. const ALFontFace* emoji_face = emoji_a->getFontFace(); LLFontBitmapCache* cache = emoji_a->getBitmapCache(); - ensure("fallback freetype exposes its face", emoji_face != nullptr); - ensure("fallback freetype exposes its atlas", cache != nullptr); + REQUIRE(emoji_face != nullptr); + REQUIRE(cache != nullptr); emoji_face->erase_glyph_entries( [target](const LLFontGlyphInfo* gi) @@ -709,9 +681,7 @@ namespace return false; }); cache->releaseSheet(target.first, static_cast(target.second)); - ensure("target sheet reports released", - cache->isSheetReleased(target.first, - static_cast(target.second))); + CHECK(cache->isSheetReleased(target.first, static_cast(target.second))); // Re-request the emoji through both heads. Each lookup must: // 1. not crash on a freed face entry, @@ -721,17 +691,11 @@ namespace kFire, EFontGlyphType::Color); const LLFontGlyphInfo* gi_b2 = head_b->getFontFreetype()->getGlyphInfo( kFire, EFontGlyphType::Color); - ensure("HeadA re-rasterized after sibling-driven eviction", - gi_a2 != nullptr); - ensure("HeadB re-rasterized after sibling-driven eviction", - gi_b2 != nullptr); + REQUIRE(gi_a2 != nullptr); + REQUIRE(gi_b2 != nullptr); const auto post_a = gi_a2->mPhaseSlots[0].mBitmapEntry; const auto post_b = gi_b2->mPhaseSlots[0].mBitmapEntry; - ensure("HeadA's post-eviction entry points at a live sheet", - !cache->isSheetReleased(post_a.first, - static_cast(post_a.second))); - ensure("HeadB's post-eviction entry points at a live sheet", - !cache->isSheetReleased(post_b.first, - static_cast(post_b.second))); + CHECK_FALSE(cache->isSheetReleased(post_a.first, static_cast(post_a.second))); + CHECK_FALSE(cache->isSheetReleased(post_b.first, static_cast(post_b.second))); } } diff --git a/indra/llrender/tests/llfontregistry_test.cpp b/indra/llrender/tests/llfontregistry_test.cpp index 76e8174079..ec1fcc68b5 100644 --- a/indra/llrender/tests/llfontregistry_test.cpp +++ b/indra/llrender/tests/llfontregistry_test.cpp @@ -40,7 +40,7 @@ #include "llxmlnode.h" #include "llsd.h" -#include "../test/lltut.h" +#include #include @@ -79,9 +79,10 @@ namespace tut return init_from_xml(®, root); } - // Friend-access wrappers so test methods (which live on the derived - // tut::object class, not on this fixture) can drive the private - // resolveFontReferences / applyFamilyOverrides via inherited members. + // Friend-access wrappers so test cases (which live on a class + // TEST_CASE_METHOD derives from this fixture, not on the fixture + // itself) can drive the private resolveFontReferences / + // applyFamilyOverrides via inherited members. void resolve(const LLSD& overrides = LLSD::emptyMap()) { reg.resolveFontReferences(overrides); @@ -163,130 +164,109 @@ namespace tut } }; - // 128 slots: TUT defaults to 50; we already have 50+ tests and want - // headroom before the silent-drop threshold bites again. - typedef test_group llfontregistry_test; - typedef llfontregistry_test::object llfontregistry_object; -namespace -{ - tut::llfontregistry_test llfontregistry_testcase("LLFontRegistry"); -} - // =================================================================== // Group 1: LLFontDescriptor (pure) // =================================================================== // normalize() pulls an embedded "Huge"/"Large"/etc. out of the name and // moves it into the size slot, leaving the family stem behind. - template<> template<> - void llfontregistry_object::test<1>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize moves a size suffix like Huge from the name to the size", "[LLFontRegistry]") { LLFontDescriptor desc("SansSerifHuge", "", 0); LLFontDescriptor n = desc.normalize(); - ensure_equals("name keeps stem", n.getName(), std::string("SansSerif")); - ensure_equals("size becomes Huge", n.getSize(), std::string("Huge")); - ensure_equals("style stays NORMAL", (S32)n.getStyle(), 0); + CHECK(n.getName() == std::string("SansSerif")); + CHECK(n.getSize() == std::string("Huge")); + UNSCOPED_INFO("style stays NORMAL"); + CHECK((S32)n.getStyle() == 0); } // "Big" is the legacy alias for "Large" — verify the rewrite happens. - template<> template<> - void llfontregistry_object::test<2>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize rewrites the legacy size suffix Big to Large", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerifBig", "", 0).normalize(); - ensure_equals("Big maps to Large", n.getSize(), std::string("Large")); - ensure_equals("name stripped", n.getName(), std::string("SansSerif")); + CHECK(n.getSize() == std::string("Large")); + CHECK(n.getName() == std::string("SansSerif")); } // "Bold"/"Italic" suffixes set the style flags AND get stripped from // the name (the leading inline comment in normalize() claims the name // is preserved, but the code definitively removes the substring). - template<> template<> - void llfontregistry_object::test<3>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize strips a Bold suffix from the name and sets the BOLD flag", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerifBold", "", 0).normalize(); - ensure_equals("Bold stripped from name", - n.getName(), std::string("SansSerif")); - ensure_equals("size defaults to Small (no size suffix in input)", - n.getSize(), std::string("Small")); - ensure_equals("BOLD flag set", (S32)n.getStyle(), (S32)LLFontGL::BOLD); + CHECK(n.getName() == std::string("SansSerif")); + UNSCOPED_INFO("size defaults to Small (no size suffix in input)"); + CHECK(n.getSize() == std::string("Small")); + CHECK((S32)n.getStyle() == (S32)LLFontGL::BOLD); } // Bold + Italic combine; other style bits (UNDERLINE) get filtered. - template<> template<> - void llfontregistry_object::test<4>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize combines Bold and Italic and drops the UNDERLINE bit", "[LLFontRegistry]") { LLFontDescriptor desc("SansSerifBoldItalic", "", static_cast(LLFontGL::UNDERLINE)); LLFontDescriptor n = desc.normalize(); - ensure_equals("BOLD|ITALIC flags", - (S32)n.getStyle(), - (S32)(LLFontGL::BOLD | LLFontGL::ITALIC)); + CHECK((S32)n.getStyle() == (S32)(LLFontGL::BOLD | LLFontGL::ITALIC)); } // "Monospace" is preserved as both name and size — the special-case // branch in normalize() exists because stripping it would leave "". - template<> template<> - void llfontregistry_object::test<5>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize keeps Monospace as both the name and the size", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("Monospace", "", 0).normalize(); - ensure_equals("name preserved", n.getName(), std::string("Monospace")); - ensure_equals("size becomes Monospace", n.getSize(), std::string("Monospace")); + CHECK(n.getName() == std::string("Monospace")); + CHECK(n.getSize() == std::string("Monospace")); } // Empty size + non-Monospace name → "Small" default. - template<> template<> - void llfontregistry_object::test<6>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize defaults an empty size to Small", "[LLFontRegistry]") { LLFontDescriptor n = LLFontDescriptor("SansSerif", "", 0).normalize(); - ensure_equals("default size is Small", n.getSize(), std::string("Small")); + CHECK(n.getSize() == std::string("Small")); } // Files attached to a descriptor survive the normalize() round-trip. - template<> template<> - void llfontregistry_object::test<7>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: normalize keeps the descriptor's font files", "[LLFontRegistry]") { LLFontDescriptor desc("SansSerif", "", 0); desc.addFontFile("Foo.ttf", EFontHinting::DEFAULT, 0, 0.f); LLFontDescriptor n = desc.normalize(); - ensure_equals("file count preserved", (S32)n.getFontFiles().size(), 1); - ensure_equals("file name preserved", - n.getFontFiles()[0].FileName, std::string("Foo.ttf")); + REQUIRE((S32)n.getFontFiles().size() == 1); + CHECK(n.getFontFiles()[0].FileName == std::string("Foo.ttf")); } // operator== compares (name, style, size). Different files but same // identity-tuple still compare equal — that's intentional, the file // list is treated as data the descriptor carries, not part of identity. - template<> template<> - void llfontregistry_object::test<8>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: operator== and hash_value ignore the font file list", "[LLFontRegistry]") { LLFontDescriptor a("SansSerif", "Medium", 0); LLFontDescriptor b("SansSerif", "Medium", 0); a.addFontFile("a.ttf", EFontHinting::DEFAULT, 0, 0.f); b.addFontFile("b.ttf", EFontHinting::DEFAULT, 0, 0.f); - ensure("equal regardless of files", a == b); - ensure_equals("hash matches", hash_value(a), hash_value(b)); + CHECK(a == b); + CHECK(hash_value(a) == hash_value(b)); } // operator< orders lexicographically by (name, style, size). Style // comparisons happen ahead of size — verify by holding name fixed. - template<> template<> - void llfontregistry_object::test<9>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: operator< orders descriptors that differ only in style", "[LLFontRegistry]") { LLFontDescriptor a("SansSerif", "Medium", 0); LLFontDescriptor b("SansSerif", "Medium", LLFontGL::BOLD); - ensure("style breaks tie before size", a < b); - ensure("strict weak ordering", !(b < a)); + CHECK(a < b); + UNSCOPED_INFO("strict weak ordering"); + CHECK_FALSE(b < a); } // The "TEMPLATE" sentinel marks descriptors that exist only as the // post-XML template (no concrete point size yet). - template<> template<> - void llfontregistry_object::test<10>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: isTemplate is true only for the TEMPLATE size sentinel", "[LLFontRegistry]") { LLFontDescriptor t("SansSerif", "TEMPLATE", 0); LLFontDescriptor n("SansSerif", "Medium", 0); - ensure("template sentinel detected", t.isTemplate()); - ensure("non-template not flagged", !n.isTemplate()); + CHECK(t.isTemplate()); + CHECK_FALSE(n.isTemplate()); } // =================================================================== @@ -295,28 +275,26 @@ namespace // populates the global size table; nameToSize hits it // when no per-family override applies. - template<> template<> - void llfontregistry_object::test<11>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: fills the global table that nameToSize reads", "[LLFontRegistry]") { const char* xml = "" " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("Medium found", reg.nameToSize("Medium", sz)); - ensure_equals("Medium = 12", sz, 12.0f); - ensure("Large found", reg.nameToSize("Large", sz)); - ensure_equals("Large = 14", sz, 14.0f); - ensure("Huge missing", !reg.nameToSize("Huge", sz)); + CHECK(reg.nameToSize("Medium", sz)); + CHECK(sz == 12.0f); + CHECK(reg.nameToSize("Large", sz)); + CHECK(sz == 14.0f); + CHECK_FALSE(reg.nameToSize("Huge", sz)); } // New-format " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Inter", LLFontGL::NORMAL); const LLFontDescriptor* bold = templateFor("Inter", LLFontGL::BOLD); - ensure("NORMAL template present", normal != nullptr); - ensure("BOLD template present", bold != nullptr); - ensure("NORMAL inherits family wght set", - normal->getFontFiles()[0].mVarAxes.wght_set); - ensure_equals("NORMAL inherits family weight 400", - normal->getFontFiles()[0].mVarAxes.wght, 400.f); - ensure_equals("NORMAL inherits family hinting light", - (S32)normal->getFontFiles()[0].mHinting, - (S32)EFontHinting::LIGHT); - ensure("BOLD has wght set after override", - bold->getFontFiles()[0].mVarAxes.wght_set); - ensure_equals("BOLD overrides weight to 700", - bold->getFontFiles()[0].mVarAxes.wght, 700.f); - ensure_equals("BOLD inherits family hinting", - (S32)bold->getFontFiles()[0].mHinting, - (S32)EFontHinting::LIGHT); + REQUIRE(normal != nullptr); + REQUIRE(bold != nullptr); + CHECK(normal->getFontFiles()[0].mVarAxes.wght_set); + CHECK(normal->getFontFiles()[0].mVarAxes.wght == 400.f); + CHECK((S32)normal->getFontFiles()[0].mHinting == (S32)EFontHinting::LIGHT); + CHECK(bold->getFontFiles()[0].mVarAxes.wght_set); + CHECK(bold->getFontFiles()[0].mVarAxes.wght == 700.f); + CHECK((S32)bold->getFontFiles()[0].mHinting == (S32)EFontHinting::LIGHT); } // children inside populate the per-family override table // that nameToSize(family, size) consults before the global table. - template<> template<> - void llfontregistry_object::test<13>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family's overrides the global size for that family only", "[LLFontRegistry]") { const char* xml = "" @@ -363,22 +332,20 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("DejaVu Medium hits family override", - reg.nameToSize("DejaVu", "Medium", sz)); - ensure_equals("DejaVu Medium = 8.6", sz, 8.6f); + CHECK(reg.nameToSize("DejaVu", "Medium", sz)); + CHECK(sz == 8.6f); F32 sz2 = 0.f; - ensure("Other family Medium falls back to global", - reg.nameToSize("SansSerif", "Medium", sz2)); - ensure_equals("Global Medium unchanged", sz2, 12.0f); + UNSCOPED_INFO("Other family Medium falls back to global"); + CHECK(reg.nameToSize("SansSerif", "Medium", sz2)); + CHECK(sz2 == 12.0f); } // Family-level ligatures="on" propagates to every file unless the // file overrides. The flag lives on LLFontFileInfo (it controls the // freetype-level setAllowMonospaceLigatures call later on). - template<> template<> - void llfontregistry_object::test<14>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family's ligatures=on carries to its files", "[LLFontRegistry]") { const char* xml = "" @@ -386,17 +353,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Mono", LLFontGL::NORMAL); - ensure("template present", d != nullptr); - ensure("ligatures inherited", - d->getFontFiles()[0].mMonospaceLigatures); + REQUIRE(d != nullptr); + CHECK(d->getFontFiles()[0].mMonospaceLigatures); } // load_collection on the family is inherited by every file; file-level // load_collection overrides per-file. Verify both paths. - template<> template<> - void llfontregistry_object::test<15>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: load_collection works on the family and on a single file", "[LLFontRegistry]") { const char* xml = "" @@ -407,18 +372,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); - ensure("family-level load_collection inherited", - templateFor("CJK")->getFontFiles()[0].mLoadCollection); - ensure("file-level load_collection works alone", - templateFor("Mono")->getFontFiles()[0].mLoadCollection); + CHECK(loadXml(xml)); + UNSCOPED_INFO("family-level load_collection inherited"); + CHECK(templateFor("CJK")->getFontFiles()[0].mLoadCollection); + UNSCOPED_INFO("file-level load_collection works alone"); + CHECK(templateFor("Mono")->getFontFiles()[0].mLoadCollection); } // flags="bold" forces LLFontGL::BOLD into the file's flags so that // a single .woff2 carrying multiple weights still lights up callers' // BOLD checks. - template<> template<> - void llfontregistry_object::test<16>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file's flags=bold sets BOLD in its flags", "[LLFontRegistry]") { const char* xml = "" @@ -428,18 +392,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Inter", LLFontGL::BOLD); - ensure("template present", d != nullptr); - ensure_equals("BOLD bit set on file flags", - d->getFontFiles()[0].mFlags & LLFontGL::BOLD, - (S32)LLFontGL::BOLD); + REQUIRE(d != nullptr); + CHECK((d->getFontFiles()[0].mFlags & LLFontGL::BOLD) == (S32)LLFontGL::BOLD); } // unicode_ranges parses comma-separated U+lo-U+hi tokens into a // CharFunctor that returns true for matching codepoints. - template<> template<> - void llfontregistry_object::test<17>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: unicode_ranges builds a CharFunctor from U+ ranges and code points", "[LLFontRegistry]") { const char* xml = "" @@ -449,21 +410,26 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const auto& cf = templateFor("Emoji")->getFontFiles()[0].CharFunctor; - ensure("functor present", static_cast(cf)); - ensure("U+2600 inside range", cf(0x2600)); - ensure("U+27BF inside range", cf(0x27BF)); - ensure("U+27C0 outside range", !cf(0x27C0)); - ensure("U+1F525 single CP", cf(0x1F525)); - ensure("0x200D via 0x prefix", cf(0x200D)); - ensure("U+0000 nowhere covered", !cf(0)); + REQUIRE(static_cast(cf)); + UNSCOPED_INFO("U+2600 inside range"); + CHECK(cf(0x2600)); + UNSCOPED_INFO("U+27BF inside range"); + CHECK(cf(0x27BF)); + UNSCOPED_INFO("U+27C0 outside range"); + CHECK_FALSE(cf(0x27C0)); + UNSCOPED_INFO("U+1F525 single CP"); + CHECK(cf(0x1F525)); + UNSCOPED_INFO("0x200D via 0x prefix"); + CHECK(cf(0x200D)); + UNSCOPED_INFO("U+0000 nowhere covered"); + CHECK_FALSE(cf(0)); } // Malformed unicode_ranges tokens get skipped with a log, but the // remaining valid tokens still build a functor. - template<> template<> - void llfontregistry_object::test<18>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: unicode_ranges skips malformed tokens and keeps the valid ones", "[LLFontRegistry]") { const char* xml = "" @@ -473,16 +439,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const auto& cf = templateFor("Frag")->getFontFiles()[0].CharFunctor; - ensure("functor still built", static_cast(cf)); - ensure("valid range survives", cf(0x2700)); + REQUIRE(static_cast(cf)); + CHECK(cf(0x2700)); } // size_delta carries to the file unchanged. createFont adds it to // the resolved point size when loading a face. - template<> template<> - void llfontregistry_object::test<19>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: size_delta carries to the file unchanged", "[LLFontRegistry]") { const char* xml = "" @@ -492,15 +457,13 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); - ensure_equals("size_delta preserved", - templateFor("X")->getFontFiles()[0].mSizeDelta, -1.5f); + CHECK(loadXml(xml)); + CHECK(templateFor("X")->getFontFiles()[0].mSizeDelta == -1.5f); } // Family metadata: label / user_selectable / monospace flow into // mFamilyMeta where getAvailableFamilies reads them. - template<> template<> - void llfontregistry_object::test<20>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getAvailableFamilies reports labels and hides user_selectable=false", "[LLFontRegistry]") { const char* xml = "" @@ -514,25 +477,25 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); // Hidden filtered out; Inter + Mono remain. Stable sort by label. - ensure_equals("Hidden filtered + 2 visible families", (S32)fams.size(), 2); + UNSCOPED_INFO("Hidden is filtered out: 2 visible families"); + CHECK((S32)fams.size() == 2); bool sawInter = false, sawMono = false; for (const auto& f : fams) { - if (f.name == "Inter") { sawInter = true; ensure_equals("Inter label", f.label, std::string("Inter Display")); } + if (f.name == "Inter") { sawInter = true; REQUIRE(f.label == std::string("Inter Display")); } if (f.name == "Mono") { sawMono = true; } } - ensure("Inter present", sawInter); - ensure("Mono present", sawMono); + CHECK(sawInter); + CHECK(sawMono); } // Two consecutive init_from_xml calls (mimicking cross-skin layering) // PREPEND the second layer's files onto an existing same-key entry. - template<> template<> - void llfontregistry_object::test<21>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a second init_from_xml layer prepends its files to the family", "[LLFontRegistry]") { const char* base = "" @@ -546,12 +509,12 @@ namespace " " " " ""; - ensure("base parse", loadXml(base)); - ensure("overlay parse", loadXml(overlay)); + CHECK(loadXml(base)); + CHECK(loadXml(overlay)); auto names = fileNames("SansSerif"); - ensure_equals("two files merged", (S32)names.size(), 2); - ensure_equals("overlay prepended", names[0], std::string("Overlay.woff2")); - ensure_equals("base kept as fallback", names[1], std::string("Base.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("Overlay.woff2")); + CHECK(names[1] == std::string("Base.woff2")); } // =================================================================== @@ -560,8 +523,7 @@ namespace // Simple appends B's files onto A's chain at the // matching style. - template<> template<> - void llfontregistry_object::test<22>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: appends the used family's files after the family's own", "[LLFontRegistry]") { const char* xml = "" @@ -573,18 +535,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("A"); - ensure_equals("A then B", (S32)names.size(), 2); - ensure_equals("A own first", names[0], std::string("A.woff2")); - ensure_equals("B appended", names[1], std::string("B.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("A.woff2")); + CHECK(names[1] == std::string("B.woff2")); } // Style fallback: A wants B at BOLD, B only declares NORMAL. // Resolver should pull B's NORMAL files into A's BOLD chain. - template<> template<> - void llfontregistry_object::test<23>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: of a family without BOLD fills the BOLD chain from its NORMAL", "[LLFontRegistry]") { const char* xml = "" @@ -596,19 +557,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("A", LLFontGL::BOLD); - ensure_equals("A-Bold then B-Regular fallback", (S32)names.size(), 2); - ensure_equals("A's bold first", names[0], std::string("A-Bold.woff2")); - ensure_equals("B NORMAL fallback", names[1], std::string("B-Regular.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("A-Bold.woff2")); + CHECK(names[1] == std::string("B-Regular.woff2")); } // Cycle A→B + B→A: each family's chain ends with the other's files // exactly once. Without the visited-set guard this would recurse // infinitely or duplicate. - template<> template<> - void llfontregistry_object::test<24>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a two-family cycle puts each file on each chain once", "[LLFontRegistry]") { const char* xml = "" @@ -621,18 +581,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); - ensure_equals("A.woff2 once on A", countFile("A", "A.woff2"), 1); - ensure_equals("B.woff2 once on A", countFile("A", "B.woff2"), 1); - ensure_equals("B.woff2 once on B", countFile("B", "B.woff2"), 1); - ensure_equals("A.woff2 once on B", countFile("B", "A.woff2"), 1); + CHECK(countFile("A", "A.woff2") == 1); + CHECK(countFile("A", "B.woff2") == 1); + CHECK(countFile("B", "B.woff2") == 1); + CHECK(countFile("B", "A.woff2") == 1); } // Diamond A→B, A→C, B→D, C→D: D must appear exactly once on A's // chain — the visited set is shared across the whole walk. - template<> template<> - void llfontregistry_object::test<25>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a diamond puts the shared family on the chain once", "[LLFontRegistry]") { const char* xml = "" @@ -653,17 +612,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); - ensure_equals("D.woff2 appears exactly once on A", - countFile("A", "D.woff2"), 1); - ensure_equals("B.woff2 once on A", countFile("A", "B.woff2"), 1); - ensure_equals("C.woff2 once on A", countFile("A", "C.woff2"), 1); + CHECK(countFile("A", "D.woff2") == 1); + CHECK(countFile("A", "B.woff2") == 1); + CHECK(countFile("A", "C.woff2") == 1); } // Self-reference logs and is skipped. - template<> template<> - void llfontregistry_object::test<26>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a of the family itself is skipped", "[LLFontRegistry]") { const char* xml = "" @@ -672,17 +629,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); - ensure_equals("only A's own file remains", - countFile("A", "A.woff2"), 1); - ensure_equals("nothing else added", - (S32)fileNames("A").size(), 1); + CHECK(countFile("A", "A.woff2") == 1); + CHECK((S32)fileNames("A").size() == 1); } // targeting an undeclared family is skipped. - template<> template<> - void llfontregistry_object::test<27>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a of an undeclared family is skipped", "[LLFontRegistry]") { const char* xml = "" @@ -691,16 +645,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); - ensure_equals("undeclared use skipped", - (S32)fileNames("A").size(), 1); + CHECK((S32)fileNames("A").size() == 1); } // -only family (no " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); - ensure("Wrapper NORMAL synthesized", - templateFor("Wrapper", LLFontGL::NORMAL) != nullptr); - ensure("Wrapper BOLD synthesized", - templateFor("Wrapper", LLFontGL::BOLD) != nullptr); + CHECK(templateFor("Wrapper", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("Wrapper", LLFontGL::BOLD) != nullptr); auto bold = fileNames("Wrapper", LLFontGL::BOLD); - ensure_equals("Wrapper BOLD pulls Inner-B", (S32)bold.size(), 1); - ensure_equals("...and the file is Inner-B", - bold[0], std::string("Inner-B.woff2")); + REQUIRE((S32)bold.size() == 1); + CHECK(bold[0] == std::string("Inner-B.woff2")); } // inherit="true" on BOLD appends the family's NORMAL files. // Verify: BOLD's chain ends with NORMAL's contents. - template<> template<> - void llfontregistry_object::test<29>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: inherit=true on BOLD appends the family's NORMAL files", "[LLFontRegistry]") { const char* xml = "" @@ -736,12 +684,12 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("F", LLFontGL::BOLD); - ensure_equals("BOLD then NORMAL via inherit", (S32)names.size(), 2); - ensure_equals("BOLD first", names[0], std::string("F-B.woff2")); - ensure_equals("NORMAL appended", names[1], std::string("F-R.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("F-B.woff2")); + CHECK(names[1] == std::string("F-R.woff2")); } // applies per-style before inherit; then inherit appends NORMAL's @@ -751,8 +699,7 @@ namespace // resolveFontReferences drops the second B because chain[0..2] already // covers everything the duplicate would; it just wastes the renderer's // per-codepoint chain walk to look up a face it'll handle the same way. - template<> template<> - void llfontregistry_object::test<30>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: BOLD inherit after keeps one copy of the used family's file", "[LLFontRegistry]") { const char* xml = "" @@ -765,24 +712,23 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto bold = fileNames("F", LLFontGL::BOLD); - ensure_equals("3 files post-dedup: own bold + B from + NORMAL inherit", - (S32)bold.size(), 3); - ensure_equals("F-B first", bold[0], std::string("F-B.woff2")); - ensure_equals("B from ", bold[1], std::string("B.woff2")); - ensure_equals("F-R inherited", bold[2], std::string("F-R.woff2")); - ensure_equals("duplicate B from NORMAL's deduped out", - countFile("F", "B.woff2", LLFontGL::BOLD), 1); + UNSCOPED_INFO("3 files post-dedup: own bold + B from + NORMAL inherit"); + REQUIRE((S32)bold.size() == 3); + CHECK(bold[0] == std::string("F-B.woff2")); + CHECK(bold[1] == std::string("B.woff2")); + CHECK(bold[2] == std::string("F-R.woff2")); + UNSCOPED_INFO("duplicate B from NORMAL's deduped out"); + CHECK(countFile("F", "B.woff2", LLFontGL::BOLD) == 1); } // Calling resolve() a second time is a no-op — mFamilyUses / // mInheritFlags are consumed at the end of the first call. Per- // family sizes (mFamilySizes) and per-file source-family tags // survive — those are runtime data, not parse state. - template<> template<> - void llfontregistry_object::test<31>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a second resolveFontReferences call adds nothing", "[LLFontRegistry]") { const char* xml = "" @@ -794,13 +740,12 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto first = fileNames("A"); resolve(); // second call must not re-append auto second = fileNames("A"); - ensure_equals("size unchanged across re-resolve", - (S32)first.size(), (S32)second.size()); + CHECK((S32)first.size() == (S32)second.size()); } // =================================================================== @@ -810,8 +755,7 @@ namespace // Family→family override: target's chain becomes [source files..., // target's own files] so source wins for the head face but target // fallbacks remain available behind it. - template<> template<> - void llfontregistry_object::test<32>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family override puts the source's files ahead of the target's", "[LLFontRegistry]") { const char* xml = "" @@ -822,23 +766,22 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("source prepended + target preserved", (S32)names.size(), 2); - ensure_equals("Alt prepended", names[0], std::string("Alt.woff2")); - ensure_equals("UI fallback kept", names[1], std::string("UI.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("Alt.woff2")); + CHECK(names[1] == std::string("UI.woff2")); } // Family→file override: the override value isn't a known family, so // it's treated as a literal file path. The synthesized LLFontFileInfo // inherits hinting/weight/flags from the target's first file so the // swap behaves like a drop-in replacement. - template<> template<> - void llfontregistry_object::test<33>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file override copies hinting/weight/ligatures from the target", "[LLFontRegistry]") { const char* xml = "" @@ -846,24 +789,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "user-supplied.ttf"; applyOverrides(overrides); const auto& files = templateFor("UI")->getFontFiles(); - ensure_equals("file added in front", files[0].FileName, - std::string("user-supplied.ttf")); - ensure_equals("hinting inherited from target's first file", - (S32)files[0].mHinting, (S32)EFontHinting::LIGHT); - ensure_equals("weight inherited", files[0].mVarAxes.wght, 400.f); - ensure("ligatures inherited", files[0].mMonospaceLigatures); + CHECK(files[0].FileName == std::string("user-supplied.ttf")); + CHECK((S32)files[0].mHinting == (S32)EFontHinting::LIGHT); + CHECK(files[0].mVarAxes.wght == 400.f); + CHECK(files[0].mMonospaceLigatures); } // Self-override (target == source string) is rejected — the override // would otherwise prepend the target's own files onto itself. - template<> template<> - void llfontregistry_object::test<34>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override of a family to itself is ignored", "[LLFontRegistry]") { const char* xml = "" @@ -871,18 +811,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "UI"; applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("self-override skipped", (S32)names.size(), 1); + CHECK((S32)names.size() == 1); } // Unknown target family: skipped, no phantom entries created. - template<> template<> - void llfontregistry_object::test<35>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override of an undeclared family creates no template", "[LLFontRegistry]") { const char* xml = "" @@ -890,36 +829,34 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["Phantom"] = "Real"; applyOverrides(overrides); - ensure("no template materialized for Phantom", - templateFor("Phantom") == nullptr); - ensure_equals("Real untouched", - (S32)fileNames("Real").size(), 1); + CHECK(templateFor("Phantom") == nullptr); + UNSCOPED_INFO("Real untouched"); + CHECK((S32)fileNames("Real").size() == 1); } // overridesEqual: snapshot of last-applied overrides updates with // each call to resolveFontReferences (which calls applyFamilyOverrides // and stashes the LLSD). - template<> template<> - void llfontregistry_object::test<36>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: overridesEqual compares against the overrides last resolved", "[LLFontRegistry]") { const char* xml = "" " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD ovr1; ovr1["UI"] = "Alt"; resolve(ovr1); - ensure("identical maps compare equal", reg.overridesEqual(ovr1)); + CHECK(reg.overridesEqual(ovr1)); LLSD ovr2; ovr2["UI"] = "Different"; - ensure("different value differs", !reg.overridesEqual(ovr2)); + CHECK_FALSE(reg.overridesEqual(ovr2)); } // applyOverrides prepends source files onto the target's chain. The @@ -929,8 +866,7 @@ namespace // own (or global) is what nameToSize returns. This pins the // post-redesign behavior: per-family size is absolute for THAT family // only, never propagated cross-family. - template<> template<> - void llfontregistry_object::test<37>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: override files carry the source family while nameToSize stays put", "[LLFontRegistry]") { const char* xml = "" @@ -943,7 +879,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; @@ -951,30 +887,28 @@ namespace // nameToSize for UI returns UI's own pin; the override does NOT // route this lookup through Alt. F32 sz = 0.f; - ensure(reg.nameToSize("UI", "Custom", sz)); - ensure_equals("UI Custom = own 9.5 (override does NOT route nameToSize)", - sz, 9.5f); + CHECK(reg.nameToSize("UI", "Custom", sz)); + UNSCOPED_INFO("UI Custom = own 9.5 (override does NOT route nameToSize)"); + CHECK(sz == 9.5f); // The override DID prepend Alt's file onto UI's chain, with // mSourceFamily=Alt — so per-file pin lookup at render time // would use Alt's for that prepended file. const auto& files = templateFor("UI")->getFontFiles(); - ensure("UI chain has at least one file", !files.empty()); - ensure_equals("first file = Alt's prepended file", - files[0].FileName, std::string("A.woff2")); - ensure_equals("prepended file tagged with source family Alt", - files[0].mSourceFamily, std::string("Alt")); + REQUIRE_FALSE(files.empty()); + CHECK(files[0].FileName == std::string("A.woff2")); + CHECK(files[0].mSourceFamily == std::string("Alt")); // Alt's pin is intact for per-file lookup. F32 alt_pin = 0.f; - ensure(familySizePt("Alt", "Custom", alt_pin)); - ensure_equals("Alt Custom pin = 11.5 (createFont uses this for Alt's file)", - alt_pin, 11.5f); + CHECK(familySizePt("Alt", "Custom", alt_pin)); + UNSCOPED_INFO("Alt Custom pin = 11.5 (createFont uses this for Alt's file)"); + CHECK(alt_pin == 11.5f); // Clear the override; UI's own pin still applies. applyOverrides(LLSD()); F32 sz2 = 0.f; - ensure(reg.nameToSize("UI", "Custom", sz2)); - ensure_equals("UI Custom = own 9.5 (override cleared)", - sz2, 9.5f); + CHECK(reg.nameToSize("UI", "Custom", sz2)); + UNSCOPED_INFO("UI Custom = own 9.5 (override cleared)"); + CHECK(sz2 == 9.5f); } // =================================================================== @@ -983,8 +917,7 @@ namespace // getMatchingFontDesc applies normalize() to its argument, so passing // an unnormalized "SansSerifHuge" finds the {SansSerif, Huge} template. - template<> template<> - void llfontregistry_object::test<38>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getMatchingFontDesc finds a declared template and misses an absent one", "[LLFontRegistry]") { const char* xml = "" @@ -992,18 +925,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLFontDescriptor query("SansSerif", "TEMPLATE", 0); - ensure("template lookup hits", reg.getMatchingFontDesc(query) != nullptr); + CHECK(reg.getMatchingFontDesc(query) != nullptr); LLFontDescriptor miss("Nope", "TEMPLATE", 0); - ensure("absent name misses", reg.getMatchingFontDesc(miss) == nullptr); + CHECK(reg.getMatchingFontDesc(miss) == nullptr); } // getClosestFontTemplate prefers the entry whose style bits overlap // most with the requested style, with BOLD as a tiebreaker. - template<> template<> - void llfontregistry_object::test<39>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getClosestFontTemplate takes an exact style and breaks ties toward BOLD", "[LLFontRegistry]") { const char* xml = "" @@ -1013,29 +945,29 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // Asking for BOLD: exact match wins. LLFontDescriptor want_bold("F", "TEMPLATE", LLFontGL::BOLD); const LLFontDescriptor* best = reg.getClosestFontTemplate(want_bold); - ensure("bold exact", best != nullptr); - ensure_equals("bold style", (S32)best->getStyle(), (S32)LLFontGL::BOLD); + REQUIRE(best != nullptr); + CHECK((S32)best->getStyle() == (S32)LLFontGL::BOLD); // Asking for BOLD|ITALIC with no BOLD|ITALIC entry: the BOLD // tiebreaker should prefer BOLD over ITALIC at equal bit-count. LLFontDescriptor want_bi("F", "TEMPLATE", (U8)(LLFontGL::BOLD | LLFontGL::ITALIC)); best = reg.getClosestFontTemplate(want_bi); - ensure("BI fallback hit", best != nullptr); - ensure_equals("BOLD wins tiebreak", - (S32)best->getStyle(), (S32)LLFontGL::BOLD); + UNSCOPED_INFO("BOLD|ITALIC request falls back to a template"); + REQUIRE(best != nullptr); + UNSCOPED_INFO("BOLD wins tiebreak"); + CHECK((S32)best->getStyle() == (S32)LLFontGL::BOLD); } // getClosestFontTemplate REJECTS entries that have style bits the // request didn't ask for. Asking for NORMAL must not return a BOLD // template even if the family has only BOLD. - template<> template<> - void llfontregistry_object::test<40>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getClosestFontTemplate never returns a style the request lacks", "[LLFontRegistry]") { const char* xml = "" @@ -1043,17 +975,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLFontDescriptor want_normal("F", "TEMPLATE", 0); - ensure("no NORMAL match returned", - reg.getClosestFontTemplate(want_normal) == nullptr); + CHECK(reg.getClosestFontTemplate(want_normal) == nullptr); } // Empty-family overload of nameToSize skips the per-family path // entirely and only consults the global table. - template<> template<> - void llfontregistry_object::test<41>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: nameToSize without a family reads only the global table", "[LLFontRegistry]") { const char* xml = "" @@ -1063,14 +993,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("empty-family overload finds Medium", - reg.nameToSize("Medium", sz)); - ensure_equals("...and gets the global value", sz, 12.0f); + CHECK(reg.nameToSize("Medium", sz)); + CHECK(sz == 12.0f); F32 sz2 = 0.f; reg.nameToSize("F", "Medium", sz2); - ensure_equals("family overload still gets per-family value", sz2, 9.0f); + UNSCOPED_INFO("family overload still gets per-family value"); + CHECK(sz2 == 9.0f); } // =================================================================== @@ -1079,8 +1009,7 @@ namespace // ANY filter returns both monospace and proportional, "default" and // user_selectable=false are excluded, and result is sorted by label. - template<> template<> - void llfontregistry_object::test<42>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getAvailableFamilies ANY drops hidden and default and sorts by label", "[LLFontRegistry]") { const char* xml = "" @@ -1097,20 +1026,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - ensure_equals("only 2 selectable families", (S32)fams.size(), 2); - ensure_equals("sorted by label, Apple first", - fams[0].name, std::string("Apple")); - ensure_equals("Zebra second", - fams[1].name, std::string("Zebra")); + REQUIRE((S32)fams.size() == 2); + UNSCOPED_INFO("sorted by label, Apple first"); + CHECK(fams[0].name == std::string("Apple")); + CHECK(fams[1].name == std::string("Zebra")); } // MONOSPACE filter returns only families with monospace="true"; // PROPORTIONAL is the inverse. - template<> template<> - void llfontregistry_object::test<43>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: getAvailableFamilies MONOSPACE and PROPORTIONAL split on monospace", "[LLFontRegistry]") { const char* xml = "" @@ -1121,22 +1048,19 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - ensure_equals("MONOSPACE: 1 family", (S32)mono.size(), 1); - ensure_equals("MONOSPACE returns Mono", - mono[0].name, std::string("Mono")); + REQUIRE((S32)mono.size() == 1); + CHECK(mono[0].name == std::string("Mono")); auto prop = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::PROPORTIONAL); - ensure_equals("PROPORTIONAL: 1 family", (S32)prop.size(), 1); - ensure_equals("PROPORTIONAL returns Sans", - prop[0].name, std::string("Sans")); + REQUIRE((S32)prop.size() == 1); + CHECK(prop[0].name == std::string("Sans")); } // Family without a label attribute defaults its UI label to the // family name. - template<> template<> - void llfontregistry_object::test<44>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family without a label uses its name as the label", "[LLFontRegistry]") { const char* xml = "" @@ -1144,20 +1068,18 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - ensure_equals("one family", (S32)fams.size(), 1); - ensure_equals("label defaults to name", - fams[0].label, std::string("Plain")); + REQUIRE((S32)fams.size() == 1); + CHECK(fams[0].label == std::string("Plain")); } // inside — literal self-reference - // by name, distinct from the 'self' alias path (test 26). The cycle + // by name, distinct from the 'self' alias path (Group 3). The cycle // detector must skip both. The post-resolve chain should contain // only A's own files, with no duplicate insertion. - template<> template<> - void llfontregistry_object::test<45>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a naming the family itself leaves only the family's own file", "[LLFontRegistry]") { const char* xml = "" @@ -1166,13 +1088,11 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("Mono"); - ensure_equals("self-reference by literal name doesn't duplicate files", - (S32)names.size(), 1); - ensure_equals("only the family's own file in chain", - names[0], std::string("M.woff2")); + REQUIRE((S32)names.size() == 1); + CHECK(names[0] == std::string("M.woff2")); } // Empty root with no children is a degenerate but valid @@ -1180,15 +1100,13 @@ namespace // and leave the registry's font map empty. Pins safe handling of // an empty skin layer (e.g., when a skin overrides only the metric // table and leaves families to inherit from a lower layer). - template<> template<> - void llfontregistry_object::test<46>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an empty root parses and resolves to no families", "[LLFontRegistry]") { const char* xml = ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto fams = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::ANY); - ensure_equals("empty XML produces empty available-families list", - (S32)fams.size(), 0); + CHECK((S32)fams.size() == 0); } // applyOverrides called twice with different LLSD maps: the second @@ -1198,8 +1116,7 @@ namespace // via the per-file source-family on the prepended head file (the // override target's chain head should reflect ONLY the most recent // override's source). - template<> template<> - void llfontregistry_object::test<47>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: applyOverrides twice replaces the first override instead of merging", "[LLFontRegistry]") { const char* xml = "" @@ -1216,7 +1133,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // First override: UI → AltA. UI's chain head becomes AltA's file. @@ -1224,11 +1141,9 @@ namespace ovr1["UI"] = "AltA"; applyOverrides(ovr1); const auto& files_a = templateFor("UI")->getFontFiles(); - ensure("UI chain non-empty after first override", !files_a.empty()); - ensure_equals("UI head = AltA's file", - files_a[0].FileName, std::string("A.woff2")); - ensure_equals("head's mSourceFamily = AltA", - files_a[0].mSourceFamily, std::string("AltA")); + REQUIRE_FALSE(files_a.empty()); + CHECK(files_a[0].FileName == std::string("A.woff2")); + CHECK(files_a[0].mSourceFamily == std::string("AltA")); // Second override: UI → AltB. The earlier UI→AltA mapping must // be replaced, not merged — UI's head is now AltB. @@ -1236,11 +1151,9 @@ namespace ovr2["UI"] = "AltB"; applyOverrides(ovr2); const auto& files_b = templateFor("UI")->getFontFiles(); - ensure("UI chain non-empty after second override", !files_b.empty()); - ensure_equals("UI head = AltB's file (replaced AltA)", - files_b[0].FileName, std::string("B.woff2")); - ensure_equals("head's mSourceFamily = AltB", - files_b[0].mSourceFamily, std::string("AltB")); + REQUIRE_FALSE(files_b.empty()); + CHECK(files_b[0].FileName == std::string("B.woff2")); + CHECK(files_b[0].mSourceFamily == std::string("AltB")); } // Override on a base family must propagate through chains to @@ -1256,8 +1169,7 @@ namespace // Plain -- + // Menu -- + + // Override Base → Custom must end up in BOTH Plain and Menu. - template<> template<> - void llfontregistry_object::test<48>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override on a base family reaches every family that s it", "[LLFontRegistry]") { const char* xml = "" @@ -1286,35 +1198,27 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD overrides; overrides["Base"] = "Custom"; resolve(overrides); // Direct target picks up the override. - ensure_equals("Base[0] is the override file", - fileNames("Base").size() > 0u - ? fileNames("Base")[0] - : std::string(), - std::string("Custom.woff2")); + CHECK((fileNames("Base").size() > 0u ? fileNames("Base")[0] : std::string()) + == std::string("Custom.woff2")); // Both dependents see Custom.woff2 in their chain — that's the // load-bearing assertion. A regression to the old order // (overrides applied after resolution) would leave them // without Custom.woff2 entirely. - ensure("Plain chain includes Custom.woff2 via Base", - countFile("Plain", "Custom.woff2") >= 1); - ensure("Menu chain includes Custom.woff2 via Base", - countFile("Menu", "Custom.woff2") >= 1); + CHECK(countFile("Plain", "Custom.woff2") >= 1); + CHECK(countFile("Menu", "Custom.woff2") >= 1); // The chain after the override-driven Base files must still // carry Source (Base's own ) and the sibling s — the // override prepends, doesn't replace. - ensure("Plain chain still has Source.woff2", - countFile("Plain", "Source.woff2") >= 1); - ensure("Plain chain still has Latin.woff2", - countFile("Plain", "Latin.woff2") >= 1); - ensure("Menu chain still has Mark.woff2", - countFile("Menu", "Mark.woff2") >= 1); + CHECK(countFile("Plain", "Source.woff2") >= 1); + CHECK(countFile("Plain", "Latin.woff2") >= 1); + CHECK(countFile("Menu", "Mark.woff2") >= 1); // Position pin: the override file MUST be at index 0 of every // dependent chain. The renderer treats position 0 as the head @@ -1323,15 +1227,11 @@ namespace // somewhere later than 0, the original Source.woff2 still wins // for every glyph it covers and the user observes "no change." const auto plain_files = fileNames("Plain"); - ensure("Plain chain non-empty", !plain_files.empty()); - ensure_equals("Plain[0] must be the override file", - plain_files.empty() ? std::string() : plain_files[0], - std::string("Custom.woff2")); + CHECK_FALSE(plain_files.empty()); + CHECK((plain_files.empty() ? std::string() : plain_files[0]) == std::string("Custom.woff2")); const auto menu_files = fileNames("Menu"); - ensure("Menu chain non-empty", !menu_files.empty()); - ensure_equals("Menu[0] must be the override file", - menu_files.empty() ? std::string() : menu_files[0], - std::string("Custom.woff2")); + CHECK_FALSE(menu_files.empty()); + CHECK((menu_files.empty() ? std::string() : menu_files[0]) == std::string("Custom.woff2")); } // Override propagation through chains for BOLD style. Real @@ -1339,8 +1239,7 @@ namespace // appends the parent NORMAL chain after the variant's own files. // Verify the override (which prepends to the BOLD template) still // ends up at position 0 of dependent chains for the BOLD style. - template<> template<> - void llfontregistry_object::test<49>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override on a base family leads the BOLD chains that it", "[LLFontRegistry]") { const char* xml = "" @@ -1363,7 +1262,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD overrides; overrides["Base"] = "Custom"; @@ -1371,15 +1270,12 @@ namespace // BOLD variant of Menu must lead with the override's BOLD file. const auto menu_bold = fileNames("Menu", LLFontGL::BOLD); - ensure("Menu BOLD chain non-empty", !menu_bold.empty()); - ensure_equals("Menu BOLD[0] must be override BOLD file", - menu_bold.empty() ? std::string() : menu_bold[0], - std::string("Custom-Bold.woff2")); + CHECK_FALSE(menu_bold.empty()); + CHECK((menu_bold.empty() ? std::string() : menu_bold[0]) == std::string("Custom-Bold.woff2")); // NORMAL variant unchanged invariant. const auto menu_normal = fileNames("Menu", LLFontGL::NORMAL); - ensure_equals("Menu NORMAL[0] must be override NORMAL file", - menu_normal.empty() ? std::string() : menu_normal[0], - std::string("Custom-Regular.woff2")); + CHECK((menu_normal.empty() ? std::string() : menu_normal[0]) + == std::string("Custom-Regular.woff2")); } // Reload with a different override: first resolve applies override A, @@ -1389,8 +1285,7 @@ namespace // user picks one font in the picker, then picks a different one — // each setting change drives LLFontRegistry::reload(font_overrides) // which clears mFontMap before re-parsing. - template<> template<> - void llfontregistry_object::test<50>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload with a new override leaves no trace of the old one", "[LLFontRegistry]") { const char* xml = "" @@ -1412,55 +1307,42 @@ namespace ""; // First load: override Base -> AltA. - ensure("first parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Base"] = "AltA"; resolve(ovr_a); const auto menu_after_a = fileNames("Menu"); - ensure("Menu chain non-empty after override A", - !menu_after_a.empty()); - ensure_equals("Menu[0] is AltA after first override", - menu_after_a.empty() ? std::string() : menu_after_a[0], - std::string("AltA.woff2")); - ensure("Menu chain has Source after first override", - countFile("Menu", "Source.woff2") >= 1); + CHECK_FALSE(menu_after_a.empty()); + CHECK((menu_after_a.empty() ? std::string() : menu_after_a[0]) == std::string("AltA.woff2")); + CHECK(countFile("Menu", "Source.woff2") >= 1); // Simulate reload: wipe parse-time state, re-parse, apply // override B. AltA must be GONE from the chain — a regression // would leak the prior override into the new resolution. simulateReload(); - ensure("re-parse ok after simulated reload", loadXml(xml)); + CHECK(loadXml(xml)); LLSD ovr_b; ovr_b["Base"] = "AltB"; resolve(ovr_b); const auto menu_after_b = fileNames("Menu"); - ensure("Menu chain non-empty after override B", - !menu_after_b.empty()); - ensure_equals("Menu[0] is AltB after second override", - menu_after_b.empty() ? std::string() : menu_after_b[0], - std::string("AltB.woff2")); - ensure_equals("AltA must NOT appear in Menu chain after replace", - countFile("Menu", "AltA.woff2"), 0); - ensure("Menu chain still has Source after replace", - countFile("Menu", "Source.woff2") >= 1); + CHECK_FALSE(menu_after_b.empty()); + CHECK((menu_after_b.empty() ? std::string() : menu_after_b[0]) == std::string("AltB.woff2")); + CHECK(countFile("Menu", "AltA.woff2") == 0); + CHECK(countFile("Menu", "Source.woff2") >= 1); // Same checks for the direct target — Base itself must show // only the new override at chain[0], no residue from AltA. const auto base_after_b = fileNames("Base"); - ensure_equals("Base[0] is AltB after second override", - base_after_b.empty() ? std::string() : base_after_b[0], - std::string("AltB.woff2")); - ensure_equals("AltA must NOT appear in Base chain after replace", - countFile("Base", "AltA.woff2"), 0); + CHECK((base_after_b.empty() ? std::string() : base_after_b[0]) == std::string("AltB.woff2")); + CHECK(countFile("Base", "AltA.woff2") == 0); } // Reload-and-clear: existing registry has override A, then user picks // "(default)" in the picker which writes an empty overrides map. // Reload with empty overrides must restore the original chain — no // override file lingers from the prior resolution. - template<> template<> - void llfontregistry_object::test<51>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload with no overrides restores the original chain head", "[LLFontRegistry]") { const char* xml = "" @@ -1478,27 +1360,22 @@ namespace " " ""; - ensure("first parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Base"] = "AltA"; resolve(ovr_a); - ensure_equals("Menu[0] is AltA before clear", - fileNames("Menu").empty() - ? std::string() - : fileNames("Menu")[0], - std::string("AltA.woff2")); + CHECK((fileNames("Menu").empty() ? std::string() : fileNames("Menu")[0]) + == std::string("AltA.woff2")); // Reload with empty overrides — picker's "(default)" sentinel. simulateReload(); - ensure("re-parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(LLSD::emptyMap()); const auto menu = fileNames("Menu"); - ensure("Menu non-empty after clear", !menu.empty()); - ensure_equals("Menu[0] returns to Source after override clear", - menu[0], std::string("Source.woff2")); - ensure_equals("AltA gone from Menu after clear", - countFile("Menu", "AltA.woff2"), 0); + REQUIRE_FALSE(menu.empty()); + CHECK(menu[0] == std::string("Source.woff2")); + CHECK(countFile("Menu", "AltA.woff2") == 0); } // Dedup pin: when the override source is also reachable through the @@ -1509,8 +1386,7 @@ namespace // is functionally redundant since chain[0] already covers everything // the duplicate would, and the duplicated entry just wastes the // renderer's per-codepoint walk. - template<> template<> - void llfontregistry_object::test<52>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override source also reached by appears once in the chain", "[LLFontRegistry]") { const char* xml = "" @@ -1528,7 +1404,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD overrides; overrides["Base"] = "DejaVu"; @@ -1536,25 +1412,21 @@ namespace // Without dedup, Menu would be [DejaVu(override), Source, // DejaVu(), …]. With dedup, the second DejaVu is dropped. - ensure_equals("Menu chain has DejaVu exactly once", - countFile("Menu", "DejaVu.woff2"), 1); - ensure("Menu chain still has Source", - countFile("Menu", "Source.woff2") >= 1); + CHECK(countFile("Menu", "DejaVu.woff2") == 1); + CHECK(countFile("Menu", "Source.woff2") >= 1); const auto menu = fileNames("Menu"); - ensure("Menu non-empty", !menu.empty()); - ensure_equals("Menu[0] is the override file", - menu[0], std::string("DejaVu.woff2")); + REQUIRE_FALSE(menu.empty()); + CHECK(menu[0] == std::string("DejaVu.woff2")); } // Pure diamond, no override. A reaches D twice — once through // B and once through C — and the dedup pass at the end of - // resolveFontReferences must drop the second copy. Test 52 covers - // the override + shape; this one pins that the dedup logic - // also operates on plain chain composition with no override - // involved, so a future regression that gates dedup on "did - // applyFamilyOverrides run?" would fail here. - template<> template<> - void llfontregistry_object::test<53>() + // resolveFontReferences must drop the second copy. The dedup pin + // above covers the override + shape; this one pins that the + // dedup logic also operates on plain chain composition with no + // override involved, so a future regression that gates dedup on + // "did applyFamilyOverrides run?" would fail here. + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a plain diamond with no override is deduplicated too", "[LLFontRegistry]") { const char* xml = "" @@ -1572,17 +1444,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // A reaches D twice (via B and via C). Pre-dedup the chain // would be [D, D]; post-dedup [D] — first hit wins, second // copy dropped. - ensure_equals("A chain has D exactly once", - countFile("A", "D.woff2"), 1); + CHECK(countFile("A", "D.woff2") == 1); const auto a_files = fileNames("A"); - ensure_equals("A chain length post-dedup", - (S32)a_files.size(), 1); + CHECK((S32)a_files.size() == 1); } // Two functored entries with the same filename must NOT collapse. @@ -1599,8 +1469,7 @@ namespace // `static_cast(a.CharFunctor) != static_cast(...)` // check passes (both are non-null), so the inner guard is the only // line keeping them apart. - template<> template<> - void llfontregistry_object::test<54>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: two entries of one file with different CharFunctors are both kept", "[LLFontRegistry]") { const char* xml = "" @@ -1619,23 +1488,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // Both entries reference the same filename and carry a non-null // CharFunctor. Dedup must keep both — collapsing would route // every codepoint through whichever functor survived, breaking // coverage for the dropped range. - ensure_equals("Combined keeps both functored entries", - countFile("Combined", "Shared.ttf"), 2); + CHECK(countFile("Combined", "Shared.ttf") == 2); } // simulateReload + re-parse with new sizes must update nameToSize for // both the global table and per-family children. // Live-fonts.xml-edit reload depends on this — without it, the rest // of reload() would swap freetypes at the OLD point sizes. - template<> template<> - void llfontregistry_object::test<55>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload with new sizes updates nameToSize for global and family sizes", "[LLFontRegistry]") { const char* xml_v1 = "" @@ -1645,13 +1512,15 @@ namespace " " " " ""; - ensure("parse v1 ok", loadXml(xml_v1)); + CHECK(loadXml(xml_v1)); resolve(); F32 sz = 0.f; - ensure(reg.nameToSize("Inter", "Foo", sz)); - ensure_equals("v1: Foo (global) = 10.0", sz, 10.0f); - ensure(reg.nameToSize("Inter", "Bar", sz)); - ensure_equals("v1: Bar (per-family) = 8.0", sz, 8.0f); + CHECK(reg.nameToSize("Inter", "Foo", sz)); + UNSCOPED_INFO("v1: Foo (global) = 10.0"); + CHECK(sz == 10.0f); + CHECK(reg.nameToSize("Inter", "Bar", sz)); + UNSCOPED_INFO("v1: Bar (per-family) = 8.0"); + CHECK(sz == 8.0f); // Now simulate a runtime fonts.xml edit: wipe state, re-parse. simulateReload(); @@ -1663,12 +1532,14 @@ namespace " " " " ""; - ensure("parse v2 ok", loadXml(xml_v2)); + CHECK(loadXml(xml_v2)); resolve(); - ensure(reg.nameToSize("Inter", "Foo", sz)); - ensure_equals("v2: Foo (global) updated to 20.0", sz, 20.0f); - ensure(reg.nameToSize("Inter", "Bar", sz)); - ensure_equals("v2: Bar (per-family) updated to 14.5", sz, 14.5f); + CHECK(reg.nameToSize("Inter", "Foo", sz)); + UNSCOPED_INFO("v2: Foo (global) updated to 20.0"); + CHECK(sz == 20.0f); + CHECK(reg.nameToSize("Inter", "Bar", sz)); + UNSCOPED_INFO("v2: Bar (per-family) updated to 14.5"); + CHECK(sz == 14.5f); } // =================================================================== @@ -1679,8 +1550,7 @@ namespace // table. No head-face chain walk, no override-source routing. Pin // assertions stay on per-file rendering (createFont path), not on // chain-head pt resolution. - template<> template<> - void llfontregistry_object::test<56>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a use-only family's nameToSize reads the global table", "[LLFontRegistry]") { const char* xml = "" @@ -1693,15 +1563,16 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); F32 sz = 0.f; - ensure(reg.nameToSize("UseOnly", "Large", sz)); - ensure_equals("UseOnly Large = global 11.0 (no chain walk)", - sz, 11.0f); + CHECK(reg.nameToSize("UseOnly", "Large", sz)); + UNSCOPED_INFO("UseOnly Large = global 11.0 (no chain walk)"); + CHECK(sz == 11.0f); // HeadFace's own still applies for direct queries. - ensure(reg.nameToSize("HeadFace", "Large", sz)); - ensure_equals("HeadFace Large = own 12.0", sz, 12.0f); + CHECK(reg.nameToSize("HeadFace", "Large", sz)); + UNSCOPED_INFO("HeadFace Large = own 12.0"); + CHECK(sz == 12.0f); } // AlchemyUIFontOverrides changes the file list but does NOT route @@ -1710,8 +1581,7 @@ namespace // for SansSerifBase still returns its own table or global — // OpenDyslexic's applies only to OD's own files (verified // by the per-file pin tests below). - template<> template<> - void llfontregistry_object::test<57>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override does not route the target's nameToSize to the source", "[LLFontRegistry]") { const char* xml = "" @@ -1724,27 +1594,26 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // Baseline: SansSerifBase has no , falls to global. F32 sz = 0.f; - ensure(reg.nameToSize("SansSerifBase", "Large", sz)); - ensure_equals("baseline SansSerifBase Large = global 11.0", - sz, 11.0f); + CHECK(reg.nameToSize("SansSerifBase", "Large", sz)); + UNSCOPED_INFO("baseline SansSerifBase Large = global 11.0"); + CHECK(sz == 11.0f); // Apply override; nameToSize result must NOT change. LLSD overrides; overrides["SansSerifBase"] = "OpenDyslexic"; applyOverrides(overrides); F32 sz2 = 0.f; - ensure(reg.nameToSize("SansSerifBase", "Large", sz2)); - ensure_equals("override DOES NOT route nameToSize through OD", - sz2, 11.0f); + CHECK(reg.nameToSize("SansSerifBase", "Large", sz2)); + UNSCOPED_INFO("override DOES NOT route nameToSize through OD"); + CHECK(sz2 == 11.0f); } // Per-family on multiple distinct families: each pins its own // family's files. The data is keyed correctly per family. - template<> template<> - void llfontregistry_object::test<58>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: each family's pin is stored under that family", "[LLFontRegistry]") { const char* xml = "" @@ -1758,13 +1627,13 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); F32 a_sz = 0.f, b_sz = 0.f; - ensure(familySizePt("A", "Large", a_sz)); - ensure(familySizePt("B", "Large", b_sz)); - ensure_equals("A Large pin = 8.0", a_sz, 8.0f); - ensure_equals("B Large pin = 13.0", b_sz, 13.0f); + CHECK(familySizePt("A", "Large", a_sz)); + CHECK(familySizePt("B", "Large", b_sz)); + CHECK(a_sz == 8.0f); + CHECK(b_sz == 13.0f); } // Per-family from a 'd family is preserved on its files @@ -1772,8 +1641,7 @@ namespace // has ; A's resolved chain has B's file with mSourceFamily=B, // and mFamilySizes[B] still has B's pin. createFont's per-file // lookup will use B's pin for B's file, regardless of A's chain pt. - template<> template<> - void llfontregistry_object::test<59>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a d family's files keep their source family and its size pin", "[LLFontRegistry]") { const char* xml = "" @@ -1787,18 +1655,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); const auto& a_files = templateFor("A")->getFontFiles(); - ensure_equals("A chain has 2 files", (S32)a_files.size(), 2); - ensure_equals("A's own file tagged 'A'", - a_files[0].mSourceFamily, std::string("A")); - ensure_equals("B's file tagged 'B' through ", - a_files[1].mSourceFamily, std::string("B")); + REQUIRE((S32)a_files.size() == 2); + CHECK(a_files[0].mSourceFamily == std::string("A")); + CHECK(a_files[1].mSourceFamily == std::string("B")); F32 b_sz = 0.f; - ensure("B's pin still in mFamilySizes", - familySizePt("B", "Large", b_sz)); - ensure_equals("B's pin = 8.0", b_sz, 8.0f); + CHECK(familySizePt("B", "Large", b_sz)); + CHECK(b_sz == 8.0f); } // Hot-reload of a per-family pin: edit the pin on a fallback @@ -1806,8 +1671,7 @@ namespace // in mFamilySizes. createFont's per-file lookup will then pick up // the new pin on the next load. Pins the runtime-edit path that // matters for fonts.xml live editing. - template<> template<> - void llfontregistry_object::test<62>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload picks up an edited per-family pin", "[LLFontRegistry]") { const char* xml_v1 = "" @@ -1817,11 +1681,11 @@ namespace " " " " ""; - ensure("parse v1 ok", loadXml(xml_v1)); + CHECK(loadXml(xml_v1)); resolve(); F32 v1 = 0.f; - ensure(familySizePt("B", "Large", v1)); - ensure_equals("v1: B Large pin = 8.0", v1, 8.0f); + CHECK(familySizePt("B", "Large", v1)); + CHECK(v1 == 8.0f); // Edit and reload — the new pin should land in mFamilySizes. simulateReload(); @@ -1833,11 +1697,11 @@ namespace " " " " ""; - ensure("parse v2 ok", loadXml(xml_v2)); + CHECK(loadXml(xml_v2)); resolve(); F32 v2 = 0.f; - ensure(familySizePt("B", "Large", v2)); - ensure_equals("v2: B Large pin = 13.5 (hot-reloaded)", v2, 13.5f); + CHECK(familySizePt("B", "Large", v2)); + CHECK(v2 == 13.5f); } // =================================================================== @@ -1847,8 +1711,7 @@ namespace // without a `name` attribute (new format): processNewFormatFont // logs a warning and bails before registering anything. No template // entry is created; sibling well-formed fonts still parse cleanly. - template<> template<> - void llfontregistry_object::test<64>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a without a name is dropped and its siblings still parse", "[LLFontRegistry]") { const char* xml = "" @@ -1859,18 +1722,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); // Empty-name template not created. The well-formed sibling is. - ensure("nameless not registered", - templateFor("", LLFontGL::NORMAL) == nullptr); - ensure("well-formed sibling parsed", - templateFor("Good", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("", LLFontGL::NORMAL) == nullptr); + CHECK(templateFor("Good", LLFontGL::NORMAL) != nullptr); } // missing either attribute is silently dropped (parser // checks both before insert). The well-formed entry parses normally. - template<> template<> - void llfontregistry_object::test<65>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a missing its name or size is dropped", "[LLFontRegistry]") { const char* xml = "" @@ -1878,17 +1738,16 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); F32 sz = 0.f; - ensure("OnlyName not added", !reg.nameToSize("OnlyName", sz)); - ensure("Both registered", reg.nameToSize("Both", sz)); - ensure_equals("Both = 13.0", sz, 13.0f); + CHECK_FALSE(reg.nameToSize("OnlyName", sz)); + CHECK(reg.nameToSize("Both", sz)); + CHECK(sz == 13.0f); } // without a `family` attribute logs a warning and is skipped. // The chain reflects only well-formed entries. - template<> template<> - void llfontregistry_object::test<66>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a without a family is skipped and the rest still resolve", "[LLFontRegistry]") { const char* xml = "" @@ -1901,19 +1760,17 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("A"); - ensure_equals("A own + Target via well-formed ", - (S32)names.size(), 2); - ensure_equals("A own first", names[0], std::string("A.woff2")); - ensure_equals("Target second", names[1], std::string("Target.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("A.woff2")); + CHECK(names[1] == std::string("Target.woff2")); } // missing name or size is dropped from the per-family table // with a warning. Sibling well-formed still applies. - template<> template<> - void llfontregistry_object::test<67>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a missing its name or size is dropped from the family table", "[LLFontRegistry]") { const char* xml = "" @@ -1925,14 +1782,13 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); F32 sz = 0.f; - ensure(reg.nameToSize("F", "Foo", sz)); - ensure_equals("F Foo = own 8.0 (well-formed entry takes effect)", - sz, 8.0f); - ensure("Malformed name 'OnlyName' did not register on F", - !reg.nameToSize("F", "OnlyName", sz)); + CHECK(reg.nameToSize("F", "Foo", sz)); + UNSCOPED_INFO("F Foo = own 8.0 (well-formed entry takes effect)"); + CHECK(sz == 8.0f); + CHECK_FALSE(reg.nameToSize("F", "OnlyName", sz)); } // " " " ""; - ensure("parse ok", loadXml(xml)); - ensure("'WEIRD' style mapped to NORMAL", - templateFor("Quirky", LLFontGL::NORMAL) != nullptr); - ensure("no BOLD template synthesized for unrecognized style", - templateFor("Quirky", LLFontGL::BOLD) == nullptr); + CHECK(loadXml(xml)); + CHECK(templateFor("Quirky", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("Quirky", LLFontGL::BOLD) == nullptr); } // Three-level cycle (A -> B -> C -> A). Visited set must // terminate the walk; each family contributes its own files exactly // once on every other family's chain. - template<> template<> - void llfontregistry_object::test<70>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a three-family cycle puts each file on the chain once", "[LLFontRegistry]") { const char* xml = "" @@ -2021,20 +1870,19 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // Each chain should contain exactly one of each file, in the // walk order starting from the queried family. - ensure_equals("A: A.woff2 once", countFile("A", "A.woff2"), 1); - ensure_equals("A: B.woff2 once", countFile("A", "B.woff2"), 1); - ensure_equals("A: C.woff2 once", countFile("A", "C.woff2"), 1); - ensure_equals("A chain length 3", (S32)fileNames("A").size(), 3); + CHECK(countFile("A", "A.woff2") == 1); + CHECK(countFile("A", "B.woff2") == 1); + CHECK(countFile("A", "C.woff2") == 1); + CHECK((S32)fileNames("A").size() == 3); } // Deep linear chain (5 levels). Walker descends without depth // restriction; final chain has each family's file exactly once. - template<> template<> - void llfontregistry_object::test<71>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a five-level chain collects every file in order", "[LLFontRegistry]") { const char* xml = "" @@ -2058,22 +1906,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto names = fileNames("F1"); - ensure_equals("F1 chain has all 5 files", (S32)names.size(), 5); - ensure_equals("F1 own first", names[0], std::string("F1.woff2")); - ensure_equals("F2 next", names[1], std::string("F2.woff2")); - ensure_equals("F3 next", names[2], std::string("F3.woff2")); - ensure_equals("F4 next", names[3], std::string("F4.woff2")); - ensure_equals("F5 last", names[4], std::string("F5.woff2")); + REQUIRE((S32)names.size() == 5); + CHECK(names[0] == std::string("F1.woff2")); + CHECK(names[1] == std::string("F2.woff2")); + CHECK(names[2] == std::string("F3.woff2")); + CHECK(names[3] == std::string("F4.woff2")); + CHECK(names[4] == std::string("F5.woff2")); } // inherit="true" on a non-NORMAL style with no NORMAL parent declared // logs a warning and is a no-op for that variant. The variant's chain // contains only its own files, not anything synthesized. - template<> template<> - void llfontregistry_object::test<72>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: inherit=true with no NORMAL style leaves the variant's own files", "[LLFontRegistry]") { const char* xml = "" @@ -2083,13 +1930,11 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto bold = fileNames("Lonely", LLFontGL::BOLD); - ensure_equals("BOLD has only its own file (no NORMAL to inherit)", - (S32)bold.size(), 1); - ensure_equals("Lonely-Bold present", - bold[0], std::string("Lonely-Bold.woff2")); + REQUIRE((S32)bold.size() == 1); + CHECK(bold[0] == std::string("Lonely-Bold.woff2")); } // Synthesis fires only for styles explicitly authored somewhere in @@ -2099,8 +1944,7 @@ namespace // (which accepts NORMAL as the closest match for any style request), // so request-time fallback handles the missing-style case rather // than synthesis aliasing every style to NORMAL. - template<> template<> - void llfontregistry_object::test<73>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a use-only family gets no template for styles its chain lacks", "[LLFontRegistry]") { const char* xml = "" @@ -2115,22 +1959,47 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); - ensure("A NORMAL synthesized (NORMAL declared in chain)", - templateFor("A", LLFontGL::NORMAL) != nullptr); - ensure("A BOLD NOT synthesized (no BOLD in chain)", - templateFor("A", LLFontGL::BOLD) == nullptr); - ensure("A ITALIC NOT synthesized (no ITALIC in chain)", - templateFor("A", LLFontGL::ITALIC) == nullptr); - ensure("A BOLD|ITALIC NOT synthesized", - templateFor("A", - static_cast(LLFontGL::BOLD | LLFontGL::ITALIC)) - == nullptr); + CHECK(templateFor("A", LLFontGL::NORMAL) != nullptr); + CHECK(templateFor("A", LLFontGL::BOLD) == nullptr); + CHECK(templateFor("A", LLFontGL::ITALIC) == nullptr); + CHECK(templateFor("A", static_cast(LLFontGL::BOLD | LLFontGL::ITALIC)) == nullptr); // NORMAL synthesizes from B and C: auto normal = fileNames("A", LLFontGL::NORMAL); - ensure_equals("A NORMAL pulls both fallbacks", - (S32)normal.size(), 2); + CHECK((S32)normal.size() == 2); + } + + // Override that points to a use-only family: applyFamilyOverrides + // recognizes the source as a known family (via mFontMap template + // presence) and prepends its resolved file list. Pins the case + // where the override source is itself a composite use-only family + // — the override should still propagate the source's chain head. + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override whose source is a use-only family prepends its chain", "[LLFontRegistry]") + { + const char* xml = + "" + " " + " " + " " + " " // use-only, source of override + " " + " " + " " + " " + " " + ""; + CHECK(loadXml(xml)); + resolve(); + LLSD overrides; + overrides["Target"] = "Composite"; + applyOverrides(overrides); + auto names = fileNames("Target"); + // Override prepends Composite's resolved chain (= Inner's files). + REQUIRE_FALSE(names.empty()); + CHECK(names[0] == std::string("Inner.woff2")); + UNSCOPED_INFO("Target's own file kept as fallback behind override"); + CHECK(countFile("Target", "Target.woff2") == 1); } // entries round-trip through the parser into mFamilySizes, @@ -2138,8 +2007,7 @@ namespace // the family name so createFont can later look up the per-family // pin at render time. Every is an absolute pin — no force // attribute needed. - template<> template<> - void llfontregistry_object::test<75>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: pins reach mFamilySizes and files carry their source family", "[LLFontRegistry]") { const char* xml = "" @@ -2149,30 +2017,28 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // entries land in mFamilySizes. F32 large = 0.f, medium = 0.f; - ensure(familySizePt("Pinned", "Large", large)); - ensure_equals("Large = 8.0", large, 8.0f); - ensure(familySizePt("Pinned", "Medium", medium)); - ensure_equals("Medium = 7.0", medium, 7.0f); + CHECK(familySizePt("Pinned", "Large", large)); + CHECK(large == 8.0f); + CHECK(familySizePt("Pinned", "Medium", medium)); + CHECK(medium == 7.0f); // Files contributed by Pinned's " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); const auto& files = templateFor("A")->getFontFiles(); - ensure_equals("A chain length 2", (S32)files.size(), 2); - ensure_equals("A's own file tagged 'A'", - files[0].mSourceFamily, std::string("A")); - ensure_equals("B's file tagged 'B' (preserved through )", - files[1].mSourceFamily, std::string("B")); + REQUIRE((S32)files.size() == 2); + CHECK(files[0].mSourceFamily == std::string("A")); + CHECK(files[1].mSourceFamily == std::string("B")); } // Override SWAP between two distinct pinned sources, mirroring the @@ -2208,8 +2072,7 @@ namespace // after the reload (parse-time data, persists). // - Target's own nameToSize is unaffected by either override — // no cross-family routing. - template<> template<> - void llfontregistry_object::test<77>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a reload swapping pinned override sources drops the first source's file", "[LLFontRegistry]") { const char* xml = "" @@ -2228,90 +2091,49 @@ namespace ""; // Boot state: override Target -> SourceA via reload-equivalent. - ensure("parse v1 ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD ovr_a; ovr_a["Target"] = "SourceA"; resolve(ovr_a); const auto& a_files = templateFor("Target")->getFontFiles(); - ensure("Target chain non-empty after first override", - !a_files.empty()); - ensure_equals("Target head = SourceA's file", - a_files[0].FileName, std::string("SourceA.woff2")); - ensure_equals("head's mSourceFamily = SourceA", - a_files[0].mSourceFamily, std::string("SourceA")); + REQUIRE_FALSE(a_files.empty()); + CHECK(a_files[0].FileName == std::string("SourceA.woff2")); + CHECK(a_files[0].mSourceFamily == std::string("SourceA")); // Picker swap: production calls reload() which wipes mFontMap + // re-parses + re-resolves with the new override. simulateReload // mirrors the wipe; loadXml + resolve mirrors the re-parse + // re-resolve. simulateReload(); - ensure("parse v2 ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD ovr_b; ovr_b["Target"] = "SourceB"; resolve(ovr_b); const auto& b_files = templateFor("Target")->getFontFiles(); - ensure("Target chain non-empty after swap", - !b_files.empty()); - ensure_equals("Target head = SourceB's file (clean swap)", - b_files[0].FileName, std::string("SourceB.woff2")); - ensure_equals("head's mSourceFamily = SourceB", - b_files[0].mSourceFamily, std::string("SourceB")); + REQUIRE_FALSE(b_files.empty()); + CHECK(b_files[0].FileName == std::string("SourceB.woff2")); + CHECK(b_files[0].mSourceFamily == std::string("SourceB")); // After the reload-driven swap, SourceA's file is gone — no // leftover state from the prior override. - ensure_equals("SourceA's file is not in the post-swap chain", - countFile("Target", "SourceA.woff2"), 0); + CHECK(countFile("Target", "SourceA.woff2") == 0); // Both source families' pins remain queryable in mFamilySizes // post-reload. createFont uses the head's mSourceFamily to // decide its render pt: under ovr_a that hits SourceA's 10, // under ovr_b that hits SourceB's 14. F32 a_pin = 0.f, b_pin = 0.f; - ensure(familySizePt("SourceA", "Large", a_pin)); - ensure_equals("SourceA pin = 10.0", a_pin, 10.0f); - ensure(familySizePt("SourceB", "Large", b_pin)); - ensure_equals("SourceB pin = 14.0", b_pin, 14.0f); + CHECK(familySizePt("SourceA", "Large", a_pin)); + CHECK(a_pin == 10.0f); + CHECK(familySizePt("SourceB", "Large", b_pin)); + CHECK(b_pin == 14.0f); // Target's own nameToSize is unaffected by either override — // it falls through to global because Target has no own . // Pins the "no cross-family routing" rule. F32 sz = 0.f; - ensure(reg.nameToSize("Target", "Large", sz)); - ensure_equals("Target Large = global 11.0 (override does not route)", - sz, 11.0f); - } - - // Override that points to a use-only family: applyFamilyOverrides - // recognizes the source as a known family (via mFontMap template - // presence) and prepends its resolved file list. Pins the case - // where the override source is itself a composite use-only family - // — the override should still propagate the source's chain head. - template<> template<> - void llfontregistry_object::test<74>() - { - const char* xml = - "" - " " - " " - " " - " " // use-only, source of override - " " - " " - " " - " " - " " - ""; - ensure("parse ok", loadXml(xml)); - resolve(); - LLSD overrides; - overrides["Target"] = "Composite"; - applyOverrides(overrides); - auto names = fileNames("Target"); - // Override prepends Composite's resolved chain (= Inner's files). - ensure("Target chain after override is non-empty", !names.empty()); - ensure_equals("Inner.woff2 prepended via Composite override", - names[0], std::string("Inner.woff2")); - ensure("Target's own file kept as fallback behind override", - countFile("Target", "Target.woff2") == 1); + CHECK(reg.nameToSize("Target", "Large", sz)); + UNSCOPED_INFO("Target Large = global 11.0 (override does not route)"); + CHECK(sz == 11.0f); } // LLFontDescriptor equality and hash are keyed on (name, style, size) @@ -2323,8 +2145,7 @@ namespace // file lists into the key, every chain mutation in the resolver becomes // a hash-bucket walk and lookups by request desc start missing. This // test fails noisily in that scenario. - template<> template<> - void llfontregistry_object::test<78>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: descriptor identity is name/style/size and a no-file lookup hits", "[LLFontRegistry]") { font_file_info_vec_t files_a; files_a.emplace_back("A.woff2", EFontHinting::FORCE_AUTOHINT, 0, 0.f); @@ -2335,18 +2156,13 @@ namespace LLFontDescriptor a("SansSerif", "Small", 0, files_a); LLFontDescriptor b("SansSerif", "Small", 0, files_b); - ensure("same (name,style,size) compares equal regardless of files", - a == b); - ensure_equals("same (name,style,size) hashes the same regardless of files", - hash_value(a), hash_value(b)); + CHECK(a == b); + CHECK(hash_value(a) == hash_value(b)); // Sanity: vary each component, equality breaks. - ensure("different name is not equal", - !(a == LLFontDescriptor("Other", "Small", 0, files_a))); - ensure("different style is not equal", - !(a == LLFontDescriptor("SansSerif", "Small", LLFontGL::BOLD, files_a))); - ensure("different size is not equal", - !(a == LLFontDescriptor("SansSerif", "Large", 0, files_a))); + CHECK_FALSE(a == LLFontDescriptor("Other", "Small", 0, files_a)); + CHECK_FALSE(a == LLFontDescriptor("SansSerif", "Small", LLFontGL::BOLD, files_a)); + CHECK_FALSE(a == LLFontDescriptor("SansSerif", "Large", 0, files_a)); // Lookup by no-files descriptor must hit a stored with-files entry. // This is what getMatchingFontDesc relies on at runtime. @@ -2354,39 +2170,36 @@ namespace m[a] = 1; LLFontDescriptor lookup("SansSerif", "Small", 0); // no files auto it = m.find(lookup); - ensure("lookup by no-files descriptor finds a stored with-files entry", - it != m.end()); + CHECK(it != m.end()); } - // overridesEqual: extra coverage beyond test<36>'s identical/different - // value pair. Pin the LLSD-comparison semantics initClass relies on + // overridesEqual: extra coverage beyond the identical/different value + // pair in Group 4. Pin the LLSD-comparison semantics initClass relies on // when deciding fast-DPI vs full-reload: empty-map and undefined-LLSD // are distinct values, and map equality is order-independent. - template<> template<> - void llfontregistry_object::test<79>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: overridesEqual tells empty from undefined and ignores key order", "[LLFontRegistry]") { const char* xml = "" " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); // Resolve with empty-map; mLastFontOverrides becomes empty-map. LLSD empty_map = LLSD::emptyMap(); resolve(empty_map); - ensure("empty map equals empty map", reg.overridesEqual(empty_map)); + CHECK(reg.overridesEqual(empty_map)); // Undefined LLSD is distinct from empty-map per llsd_equals. LLSD undef; - ensure("empty map differs from undefined LLSD", - !reg.overridesEqual(undef)); + CHECK_FALSE(reg.overridesEqual(undef)); // Map equality is keyed by (key,value) pairs regardless of LLSD's // internal iteration order. Pin so a re-ordered override map // (e.g. picker rebuilds the map in different order) doesn't trip // the fast-path and force a redundant full reload. simulateReload(); - ensure("re-parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD ovr1; ovr1["UI"] = "Alt"; ovr1["Other"] = "Pick"; @@ -2394,8 +2207,7 @@ namespace ovr2["Other"] = "Pick"; ovr2["UI"] = "Alt"; resolve(ovr1); - ensure("LLSD map equality is order-independent", - reg.overridesEqual(ovr2)); + CHECK(reg.overridesEqual(ovr2)); } // sweepGlyphCaches must not dereference NULL-valued template entries. @@ -2404,25 +2216,23 @@ namespace // sweep during early frames before any head exists. The NULL guard at // llfontregistry.cpp:1595 covers heads; this test pins the empty- // fallback-cache path too. Crash here = regression. - template<> template<> - void llfontregistry_object::test<80>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: sweepGlyphCaches survives a registry holding only templates", "[LLFontRegistry]") { const char* xml = "" " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // No heads instantiated, no fallback-cache entries. reg.sweepGlyphCaches(); - ensure("registry survives sweep with templates only", true); + CHECK(true); } // OpenType variation axes plumbed through XML parse. Family-level // font_italic / font_width / font_slant cascade to child // entries that don't override; per-file overrides win when present. - template<> template<> - void llfontregistry_object::test<81>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: font_italic/font_width/font_slant cascade to files unless overridden", "[LLFontRegistry]") { const char* xml = "" @@ -2435,44 +2245,35 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Demo", LLFontGL::NORMAL); - ensure("NORMAL template present", normal != nullptr); + REQUIRE(normal != nullptr); const auto& nfiles = normal->getFontFiles(); - ensure_equals("NORMAL has one file", nfiles.size(), 1u); - ensure("NORMAL inherits family ital", - nfiles[0].mVarAxes.ital_set); - ensure_equals("NORMAL ital == 1.0", nfiles[0].mVarAxes.ital, 1.0f); - ensure("NORMAL inherits family wdth", - nfiles[0].mVarAxes.wdth_set); - ensure_equals("NORMAL wdth == 87.5", nfiles[0].mVarAxes.wdth, 87.5f); - ensure("NORMAL inherits family slnt", - nfiles[0].mVarAxes.slnt_set); - ensure_equals("NORMAL slnt == -12.0", nfiles[0].mVarAxes.slnt, -12.0f); + REQUIRE(nfiles.size() == 1u); + CHECK(nfiles[0].mVarAxes.ital_set); + CHECK(nfiles[0].mVarAxes.ital == 1.0f); + CHECK(nfiles[0].mVarAxes.wdth_set); + CHECK(nfiles[0].mVarAxes.wdth == 87.5f); + CHECK(nfiles[0].mVarAxes.slnt_set); + CHECK(nfiles[0].mVarAxes.slnt == -12.0f); // Per-file override on wdth wins; ital/slnt still inherit. const LLFontDescriptor* bold = templateFor("Demo", LLFontGL::BOLD); - ensure("BOLD template present", bold != nullptr); + REQUIRE(bold != nullptr); const auto& wfiles = bold->getFontFiles(); - ensure_equals("BOLD has one file", wfiles.size(), 1u); - ensure_equals("BOLD overrides wdth to 125.0", - wfiles[0].mVarAxes.wdth, 125.0f); - ensure("BOLD keeps inherited ital", - wfiles[0].mVarAxes.ital_set); - ensure_equals("BOLD inherited ital still 1.0", - wfiles[0].mVarAxes.ital, 1.0f); - ensure("BOLD keeps inherited slnt", - wfiles[0].mVarAxes.slnt_set); - ensure_equals("BOLD inherited slnt still -12.0", - wfiles[0].mVarAxes.slnt, -12.0f); + REQUIRE(wfiles.size() == 1u); + CHECK(wfiles[0].mVarAxes.wdth == 125.0f); + CHECK(wfiles[0].mVarAxes.ital_set); + CHECK(wfiles[0].mVarAxes.ital == 1.0f); + CHECK(wfiles[0].mVarAxes.slnt_set); + CHECK(wfiles[0].mVarAxes.slnt == -12.0f); } // Files without any axis attribute leave ALFontVarAxes at its // default (no *_set flags). Pins the negative path that the cache // key + face-load skip rely on. - template<> template<> - void llfontregistry_object::test<82>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file with no axis attributes leaves every ALFontVarAxes flag unset", "[LLFontRegistry]") { const char* xml = "" @@ -2480,22 +2281,21 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const LLFontDescriptor* d = templateFor("Plain", LLFontGL::NORMAL); - ensure("template present", d != nullptr); + REQUIRE(d != nullptr); const auto& f = d->getFontFiles()[0]; - ensure("no wght", !f.mVarAxes.wght_set); - ensure("no opsz", !f.mVarAxes.opsz_set); - ensure("no ital", !f.mVarAxes.ital_set); - ensure("no wdth", !f.mVarAxes.wdth_set); - ensure("no slnt", !f.mVarAxes.slnt_set); + CHECK_FALSE(f.mVarAxes.wght_set); + CHECK_FALSE(f.mVarAxes.opsz_set); + CHECK_FALSE(f.mVarAxes.ital_set); + CHECK_FALSE(f.mVarAxes.wdth_set); + CHECK_FALSE(f.mVarAxes.slnt_set); } // font_optical_size cascades family -> file the same way // font_weight does. Per-file override wins; absence at file level // inherits the family value. Pins the new opsz axis plumbing. - template<> template<> - void llfontregistry_object::test<83>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: font_optical_size cascades to files unless a file overrides it", "[LLFontRegistry]") { const char* xml = "" @@ -2508,32 +2308,27 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); const LLFontDescriptor* normal = templateFor("Demo", LLFontGL::NORMAL); - ensure("NORMAL template present", normal != nullptr); + REQUIRE(normal != nullptr); const auto& nfiles = normal->getFontFiles(); - ensure_equals("NORMAL has one file", nfiles.size(), 1u); - ensure("NORMAL inherits family opsz", - nfiles[0].mVarAxes.opsz_set); - ensure_equals("NORMAL opsz == 14.0", - nfiles[0].mVarAxes.opsz, 14.0f); + REQUIRE(nfiles.size() == 1u); + CHECK(nfiles[0].mVarAxes.opsz_set); + CHECK(nfiles[0].mVarAxes.opsz == 14.0f); const LLFontDescriptor* bold = templateFor("Demo", LLFontGL::BOLD); - ensure("BOLD template present", bold != nullptr); + REQUIRE(bold != nullptr); const auto& bfiles = bold->getFontFiles(); - ensure_equals("BOLD has one file", bfiles.size(), 1u); - ensure("BOLD has opsz set after override", - bfiles[0].mVarAxes.opsz_set); - ensure_equals("BOLD overrides opsz to 28.0", - bfiles[0].mVarAxes.opsz, 28.0f); + REQUIRE(bfiles.size() == 1u); + CHECK(bfiles[0].mVarAxes.opsz_set); + CHECK(bfiles[0].mVarAxes.opsz == 28.0f); } // replace_first=true on a family→family override drops the target's - // head file before prepending the source. Mirror of test<32> with - // the new map shape. - template<> template<> - void llfontregistry_object::test<84>() + // head file before prepending the source. Mirror of Group 4's + // family→family override case with the new map shape. + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first drops the target's head before prepending the source", "[LLFontRegistry]") { const char* xml = "" @@ -2544,7 +2339,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; LLSD entry = LLSD::emptyMap(); @@ -2553,15 +2348,13 @@ namespace overrides["UI"] = entry; applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("head replaced -> only one file remains", - (S32)names.size(), 1); - ensure_equals("Alt is the head", names[0], std::string("Alt.woff2")); + REQUIRE((S32)names.size() == 1); + CHECK(names[0] == std::string("Alt.woff2")); } // replace_first=true preserves orig_files[1..]. Verifies the // tail-merge contract: drop only the head, keep DejaVu/Emoji/CJK. - template<> template<> - void llfontregistry_object::test<85>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first keeps the target's fallbacks after its head", "[LLFontRegistry]") { const char* xml = "" @@ -2576,13 +2369,12 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // Sanity: post-resolve UI's chain is [UI.woff2, DejaVu.woff2]. auto pre_names = fileNames("UI"); - ensure_equals("UI starts with 2 files", (S32)pre_names.size(), 2); - ensure_equals("UI head is UI.woff2", - pre_names[0], std::string("UI.woff2")); + REQUIRE((S32)pre_names.size() == 2); + CHECK(pre_names[0] == std::string("UI.woff2")); LLSD overrides; LLSD entry = LLSD::emptyMap(); @@ -2592,18 +2384,15 @@ namespace applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("[Alt, DejaVu] -> 2 files", - (S32)names.size(), 2); - ensure_equals("Alt is the head", names[0], std::string("Alt.woff2")); - ensure_equals("DejaVu fallback preserved", - names[1], std::string("DejaVu.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("Alt.woff2")); + CHECK(names[1] == std::string("DejaVu.woff2")); } // replace_first=true with family→file override: the target's head // is dropped and the synthesized override file takes its slot; // tail still preserved. - template<> template<> - void llfontregistry_object::test<86>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first with a file override puts the file in the head's slot", "[LLFontRegistry]") { const char* xml = "" @@ -2615,7 +2404,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; @@ -2626,19 +2415,16 @@ namespace applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("[user.ttf, DejaVu] -> 2 files", - (S32)names.size(), 2); - ensure_equals("user.ttf head", names[0], std::string("user.ttf")); - ensure_equals("DejaVu fallback preserved", - names[1], std::string("DejaVu.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("user.ttf")); + CHECK(names[1] == std::string("DejaVu.woff2")); } // replace_first=false in the map shape behaves identically to the // bare string shape: the override is prepended onto the chain and // the target's head stays as a fallback. Pin the legacy semantics // on the new map shape so it can't accidentally diverge. - template<> template<> - void llfontregistry_object::test<87>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first=false prepends like the bare string shape", "[LLFontRegistry]") { const char* xml = "" @@ -2649,7 +2435,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; @@ -2660,17 +2446,14 @@ namespace applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("map(replace_first=false) -> [Alt, UI]", - (S32)names.size(), 2); - ensure_equals("Alt prepended", names[0], std::string("Alt.woff2")); - ensure_equals("UI head preserved as fallback (no replace)", - names[1], std::string("UI.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("Alt.woff2")); + CHECK(names[1] == std::string("UI.woff2")); } // String shape continues to prepend (no replace). Regression guard // for the most common user-facing override path. - template<> template<> - void llfontregistry_object::test<88>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a bare string override still prepends without replacing", "[LLFontRegistry]") { const char* xml = "" @@ -2681,16 +2464,15 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); LLSD overrides; overrides["UI"] = "Alt"; // bare string applyOverrides(overrides); auto names = fileNames("UI"); - ensure_equals("string shape -> [Alt, UI]", (S32)names.size(), 2); - ensure_equals("Alt prepended", names[0], std::string("Alt.woff2")); - ensure_equals("UI head preserved as fallback", - names[1], std::string("UI.woff2")); + REQUIRE((S32)names.size() == 2); + CHECK(names[0] == std::string("Alt.woff2")); + CHECK(names[1] == std::string("UI.woff2")); } // attaches a CharFunctor to every file @@ -2698,8 +2480,7 @@ namespace // `` // ends up with EmojiBase's file gated by that range — accepts an // astral codepoint, rejects ASCII. - template<> template<> - void llfontregistry_object::test<89>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: gates the used family's files with a CharFunctor", "[LLFontRegistry]") { const char* xml = "" @@ -2711,27 +2492,23 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Consumer"); - ensure("template present", d != nullptr); + REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); - ensure_equals("Consumer has 2 files", (S32)files.size(), 2); - ensure_equals("UI head", files[0].FileName, std::string("UI.woff2")); - ensure_equals("Noto from EmojiBase", files[1].FileName, std::string("Noto.ttf")); - ensure("Noto has a use-level functor", - static_cast(files[1].CharFunctor)); - ensure("functor accepts U+1F600", - files[1].CharFunctor((llwchar)0x1F600)); - ensure("functor rejects ASCII", - !files[1].CharFunctor((llwchar)0x0041)); + REQUIRE((S32)files.size() == 2); + CHECK(files[0].FileName == std::string("UI.woff2")); + CHECK(files[1].FileName == std::string("Noto.ttf")); + REQUIRE(static_cast(files[1].CharFunctor)); + CHECK(files[1].CharFunctor((llwchar)0x1F600)); + CHECK_FALSE(files[1].CharFunctor((llwchar)0x0041)); } // Composition: file's own unicode_ranges intersects with the // use-level filter. Both gates must accept a codepoint for it to // pass. - template<> template<> - void llfontregistry_object::test<90>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a file's unicode_ranges and the filter must both accept", "[LLFontRegistry]") { const char* xml = "" @@ -2744,26 +2521,25 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Consumer"); - ensure("template present", d != nullptr); + REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); - ensure_equals("Consumer has 1 file", (S32)files.size(), 1); - ensure("composed functor accepts U+1F500 (in both)", - files[0].CharFunctor((llwchar)0x1F500)); - ensure("composed functor rejects U+1F100 (in file, NOT in use)", - !files[0].CharFunctor((llwchar)0x1F100)); - ensure("composed functor rejects U+1F900 (in use, NOT in file)", - !files[0].CharFunctor((llwchar)0x1F900)); - ensure("composed functor rejects U+0041 (in neither)", - !files[0].CharFunctor((llwchar)0x0041)); + REQUIRE((S32)files.size() == 1); + UNSCOPED_INFO("composed functor accepts U+1F500 (in both)"); + CHECK(files[0].CharFunctor((llwchar)0x1F500)); + UNSCOPED_INFO("composed functor rejects U+1F100 (in file, NOT in use)"); + CHECK_FALSE(files[0].CharFunctor((llwchar)0x1F100)); + UNSCOPED_INFO("composed functor rejects U+1F900 (in use, NOT in file)"); + CHECK_FALSE(files[0].CharFunctor((llwchar)0x1F900)); + UNSCOPED_INFO("composed functor rejects U+0041 (in neither)"); + CHECK_FALSE(files[0].CharFunctor((llwchar)0x0041)); } // Two consumers share one source with different filters. Each // resolved chain has its own functor — no cross-talk. - template<> template<> - void llfontregistry_object::test<91>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: two rs of one family keep their own filters", "[LLFontRegistry]") { const char* xml = "" @@ -2777,27 +2553,26 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); const auto& bf = templateFor("Broad")->getFontFiles(); const auto& nf = templateFor("Narrow")->getFontFiles(); - ensure_equals("Broad has 1 file", (S32)bf.size(), 1); - ensure_equals("Narrow has 1 file", (S32)nf.size(), 1); + REQUIRE((S32)bf.size() == 1); + REQUIRE((S32)nf.size() == 1); // Broad accepts U+1F100; Narrow doesn't. - ensure("Broad accepts U+1F100", bf[0].CharFunctor((llwchar)0x1F100)); - ensure("Narrow rejects U+1F100", !nf[0].CharFunctor((llwchar)0x1F100)); + CHECK(bf[0].CharFunctor((llwchar)0x1F100)); + CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); // Both accept U+1F600 (in their own range). - ensure("Broad accepts U+1F600", bf[0].CharFunctor((llwchar)0x1F600)); - ensure("Narrow accepts U+1F600", nf[0].CharFunctor((llwchar)0x1F600)); + CHECK(bf[0].CharFunctor((llwchar)0x1F600)); + CHECK(nf[0].CharFunctor((llwchar)0x1F600)); } // Single override on the source family propagates to every consumer // with each consumer's filter intact. This is the load-bearing // motivation for use-level filters: one override entry, two chains // updated, each with its own glyph gate. - template<> template<> - void llfontregistry_object::test<92>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override on a used family reaches each r with its filter", "[LLFontRegistry]") { const char* xml = "" @@ -2811,7 +2586,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); // Override at the SOURCE — EmojiBase gets prepended with Twemoji. // Apply BEFORE resolveFontReferences so the consumers' use chains @@ -2827,27 +2602,20 @@ namespace // Each consumer sees BOTH files (Twemoji prepended onto the // EmojiBase chain by the override; Noto preserved behind it), // each with the consumer's filter applied. - ensure_equals("Broad has 2 files (Twemoji + Noto)", - (S32)bf.size(), 2); - ensure_equals("Broad head is Twemoji", - bf[0].FileName, std::string("Twemoji.ttf")); - ensure_equals("Narrow has 2 files", - (S32)nf.size(), 2); - ensure_equals("Narrow head is Twemoji", - nf[0].FileName, std::string("Twemoji.ttf")); + REQUIRE((S32)bf.size() == 2); + CHECK(bf[0].FileName == std::string("Twemoji.ttf")); + REQUIRE((S32)nf.size() == 2); + CHECK(nf[0].FileName == std::string("Twemoji.ttf")); // Filter still gates each consumer independently. - ensure("Broad's Twemoji accepts U+1F100", - bf[0].CharFunctor((llwchar)0x1F100)); - ensure("Narrow's Twemoji rejects U+1F100", - !nf[0].CharFunctor((llwchar)0x1F100)); + CHECK(bf[0].CharFunctor((llwchar)0x1F100)); + CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); } // Combined replace_first + EmojiBase: { value: "Twemoji.ttf", // replace_first: true } on EmojiBase fully displaces Noto rather // than prepending in front of it. Each consumer's chain ends up // with just the override file gated by the consumer's filter. - template<> template<> - void llfontregistry_object::test<93>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: replace_first on a used family displaces its file in every r", "[LLFontRegistry]") { const char* xml = "" @@ -2861,7 +2629,7 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); LLSD overrides; LLSD entry = LLSD::emptyMap(); @@ -2874,21 +2642,14 @@ namespace const auto& nf = templateFor("Narrow")->getFontFiles(); // replace_first dropped Noto from EmojiBase's chain; consumers // pull just Twemoji through their respective use-level filters. - ensure_equals("Broad has 1 file (Noto displaced)", - (S32)bf.size(), 1); - ensure_equals("Broad head is Twemoji", - bf[0].FileName, std::string("Twemoji.ttf")); - ensure_equals("Narrow has 1 file (Noto displaced)", - (S32)nf.size(), 1); - ensure_equals("Narrow head is Twemoji", - nf[0].FileName, std::string("Twemoji.ttf")); - ensure("Broad's Twemoji accepts U+1F100", - bf[0].CharFunctor((llwchar)0x1F100)); - ensure("Narrow's Twemoji rejects U+1F100", - !nf[0].CharFunctor((llwchar)0x1F100)); - ensure("Both accept U+1F600", - bf[0].CharFunctor((llwchar)0x1F600) - && nf[0].CharFunctor((llwchar)0x1F600)); + REQUIRE((S32)bf.size() == 1); + CHECK(bf[0].FileName == std::string("Twemoji.ttf")); + REQUIRE((S32)nf.size() == 1); + CHECK(nf[0].FileName == std::string("Twemoji.ttf")); + CHECK(bf[0].CharFunctor((llwchar)0x1F100)); + CHECK_FALSE(nf[0].CharFunctor((llwchar)0x1F100)); + CHECK(bf[0].CharFunctor((llwchar)0x1F600)); + CHECK(nf[0].CharFunctor((llwchar)0x1F600)); } // No-cascade regression: a use-level filter applies ONLY to the @@ -2897,8 +2658,7 @@ namespace // explicit design rule (filter is local to the use site) — pin it // so a future "fix" that propagates the filter through the chain // doesn't silently change semantics for existing chains. - template<> template<> - void llfontregistry_object::test<94>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a filter does not cascade into the used family's own s", "[LLFontRegistry]") { const char* xml = "" @@ -2913,14 +2673,14 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); const LLFontDescriptor* d = templateFor("Outer"); - ensure("Outer template present", d != nullptr); + REQUIRE(d != nullptr); const auto& files = d->getFontFiles(); // Outer pulls Mid's own files (Mid.ttf) PLUS Mid's chain // (Inner.ttf). The filter applies only to Mid's direct files. - ensure_equals("Outer has 2 files", (S32)files.size(), 2); + CHECK((S32)files.size() == 2); // Locate the entries by filename rather than positional index — // collect_chain orders direct files before use-chain files @@ -2933,104 +2693,23 @@ namespace if (f.FileName == "Mid.ttf") mid = &f; if (f.FileName == "Inner.ttf") inner = &f; } - ensure("Mid.ttf contributed", mid != nullptr); - ensure("Inner.ttf contributed", inner != nullptr); + REQUIRE(mid != nullptr); + REQUIRE(inner != nullptr); // Mid is filtered (use-level filter applied to its own files). - ensure("Mid has functor", static_cast(mid->CharFunctor)); - ensure("Mid functor accepts U+1F600", mid->CharFunctor((llwchar)0x1F600)); - ensure("Mid functor rejects U+0041", !mid->CharFunctor((llwchar)0x0041)); + REQUIRE(static_cast(mid->CharFunctor)); + CHECK(mid->CharFunctor((llwchar)0x1F600)); + CHECK_FALSE(mid->CharFunctor((llwchar)0x0041)); // Inner is NOT filtered — the use-level filter doesn't cascade. - ensure("Inner has no functor (no cascade)", - !static_cast(inner->CharFunctor)); - } - - // Baseline: a use-only EmojiBase that s NotoEmoji, consumed - // by Emoji with a unicode_ranges filter. Emoji's resolved chain - // contains exactly one Noto file (contributed via the deepest - // step, with no filter — the use-level range is dropped on - // recursion per the explicit design rule, see test<94>). - template<> template<> - void llfontregistry_object::test<98>() - { - const char* xml = - "" - " " - " " - " " - " " - " " - " " - " " - " " - " " - ""; - ensure("parse ok", loadXml(xml)); - resolve(); - const auto& files = templateFor("Emoji")->getFontFiles(); - ensure_equals("Emoji has exactly 1 file (baseline)", - (S32)files.size(), 1); - ensure_equals("the file is Noto.ttf", - files[0].FileName, std::string("Noto.ttf")); - ensure("Noto fallback has NO functor (recursion dropped use-level filter)", - !static_cast(files[0].CharFunctor)); - } - - // Override target already s the override source: a use-only - // EmojiBase that s NotoEmoji, with override EmojiBase=NotoEmoji - // (replace_first=true, matching the production picker write for - // EmojiBase). Mirrors the production workflow Preferences > Themes - // > Emoji Font: pick "Noto Emoji" while EmojiBase already s - // NotoEmoji underneath. - // - // Expected (no-op): Emoji's resolved chain stays at 1 file — - // identical to the baseline above — because the override source is - // what EmojiBase already pulls through . - // - // Observed pre-fix: Emoji's chain ends up with TWO copies of the - // file — one filtered (the override-prepended file gated by the - // use-level range), one unfiltered (the recursion - // drops the use-level filter). Step-4 dedup keeps both because - // their CharFunctors differ. The duplicate filtered head perturbs - // the shape-itemizer priority (priority-1 functored hit before the - // priority-3 no-functor hit) and is observable as differing emoji - // metrics through the codepoint vs shape paths. - template<> template<> - void llfontregistry_object::test<99>() - { - const char* xml = - "" - " " - " " - " " - " " - " " - " " - " " - " " - " " - ""; - ensure("parse ok", loadXml(xml)); - LLSD overrides; - LLSD entry = LLSD::emptyMap(); - entry["value"] = "NotoEmoji"; - entry["replace_first"] = true; - overrides["EmojiBase"] = entry; - resolve(overrides); - - const auto& files = templateFor("Emoji")->getFontFiles(); - ensure_equals("Emoji chain length unchanged by no-op override " - "(EmojiBase already s NotoEmoji)", - (S32)files.size(), 1); + CHECK_FALSE(static_cast(inner->CharFunctor)); } // emoji="true" attribute parses; getAvailableFamilies(EMOJI) returns // only emoji-flagged families. PROPORTIONAL and MONOSPACE filter // emoji families OUT so they don't surface in regular text-font // pickers (a color-emoji file shouldn't appear as a UI choice). - template<> template<> - void llfontregistry_object::test<95>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: emoji=true families list under EMOJI and drop out of the others", "[LLFontRegistry]") { const char* xml = "" @@ -3047,38 +2726,33 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); // EMOJI returns the two emoji families, sorted by label. auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - ensure_equals("EMOJI: 2 families", (S32)em.size(), 2); + CHECK((S32)em.size() == 2); std::set em_names; for (const auto& f : em) em_names.insert(f.name); - ensure("EMOJI returns NotoEmoji", em_names.count("NotoEmoji") == 1); - ensure("EMOJI returns Twemoji", em_names.count("Twemoji") == 1); + CHECK(em_names.count("NotoEmoji") == 1); + CHECK(em_names.count("Twemoji") == 1); // PROPORTIONAL excludes both monospace AND emoji. auto prop = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::PROPORTIONAL); - ensure_equals("PROPORTIONAL: 1 family (just Sans)", - (S32)prop.size(), 1); - ensure_equals("PROPORTIONAL returns Sans", - prop[0].name, std::string("Sans")); + REQUIRE((S32)prop.size() == 1); + CHECK(prop[0].name == std::string("Sans")); // MONOSPACE excludes emoji families even if they were also // marked monospace=true (emoji wins for filter routing). auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - ensure_equals("MONOSPACE: 1 family (just Mono)", - (S32)mono.size(), 1); - ensure_equals("MONOSPACE returns Mono", - mono[0].name, std::string("Mono")); + REQUIRE((S32)mono.size() == 1); + CHECK(mono[0].name == std::string("Mono")); } // emoji="true" + monospace="true" on the same family routes to // EMOJI only, not MONOSPACE. Locks the "emoji wins" rule from the // FamilyFilter docs. - template<> template<> - void llfontregistry_object::test<96>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a family both emoji and monospace lists under EMOJI only", "[LLFontRegistry]") { const char* xml = "" @@ -3086,22 +2760,19 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - ensure_equals("EMOJI returns the dual-flagged family", - (S32)em.size(), 1); + CHECK((S32)em.size() == 1); auto mono = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::MONOSPACE); - ensure_equals("MONOSPACE excludes the dual-flagged family", - (S32)mono.size(), 0); + CHECK((S32)mono.size() == 0); } // user_selectable="false" still hides a family from EMOJI just like // it hides from PROPORTIONAL/MONOSPACE. Pin so internal aliases // (EmojiBase, Emoji, LimitedEmoji) don't surface even if some // future change marks them emoji=true. - template<> template<> - void llfontregistry_object::test<97>() + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: user_selectable=false hides a family from EMOJI too", "[LLFontRegistry]") { const char* xml = "" @@ -3112,12 +2783,82 @@ namespace " " " " ""; - ensure("parse ok", loadXml(xml)); + CHECK(loadXml(xml)); resolve(); auto em = reg.getAvailableFamilies(LLFontRegistry::FamilyFilter::EMOJI); - ensure_equals("EMOJI: only the user-selectable family", - (S32)em.size(), 1); - ensure_equals("EMOJI returns Public", - em[0].name, std::string("Public")); + REQUIRE((S32)em.size() == 1); + CHECK(em[0].name == std::string("Public")); + } + + // Baseline: a use-only EmojiBase that s NotoEmoji, consumed + // by Emoji with a unicode_ranges filter. Emoji's resolved chain + // contains exactly one Noto file (contributed via the deepest + // step, with no filter — the use-level range is dropped on + // recursion per the explicit design rule, see the no-cascade case above). + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: a filter on a use-only family leaves the deeper file unfiltered", "[LLFontRegistry]") + { + const char* xml = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + CHECK(loadXml(xml)); + resolve(); + const auto& files = templateFor("Emoji")->getFontFiles(); + REQUIRE((S32)files.size() == 1); + CHECK(files[0].FileName == std::string("Noto.ttf")); + CHECK_FALSE(static_cast(files[0].CharFunctor)); + } + + // Override target already s the override source: a use-only + // EmojiBase that s NotoEmoji, with override EmojiBase=NotoEmoji + // (replace_first=true, matching the production picker write for + // EmojiBase). Mirrors the production workflow Preferences > Themes + // > Emoji Font: pick "Noto Emoji" while EmojiBase already s + // NotoEmoji underneath. + // + // Expected (no-op): Emoji's resolved chain stays at 1 file — + // identical to the baseline above — because the override source is + // what EmojiBase already pulls through . + // + // Observed pre-fix: Emoji's chain ends up with TWO copies of the + // file — one filtered (the override-prepended file gated by the + // use-level range), one unfiltered (the recursion + // drops the use-level filter). Step-4 dedup keeps both because + // their CharFunctors differ. The duplicate filtered head perturbs + // the shape-itemizer priority (priority-1 functored hit before the + // priority-3 no-functor hit) and is observable as differing emoji + // metrics through the codepoint vs shape paths. + TEST_CASE_METHOD(llfontregistry_data, "LLFontRegistry: an override to a family the target already s adds no file", "[LLFontRegistry]") + { + const char* xml = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + CHECK(loadXml(xml)); + LLSD overrides; + LLSD entry = LLSD::emptyMap(); + entry["value"] = "NotoEmoji"; + entry["replace_first"] = true; + overrides["EmojiBase"] = entry; + resolve(overrides); + + const auto& files = templateFor("Emoji")->getFontFiles(); + CHECK((S32)files.size() == 1); } } diff --git a/indra/llrender/tests/llfonttextcache_test.cpp b/indra/llrender/tests/llfonttextcache_test.cpp index a5f7540dd1..bae46234e5 100644 --- a/indra/llrender/tests/llfonttextcache_test.cpp +++ b/indra/llrender/tests/llfonttextcache_test.cpp @@ -46,7 +46,7 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include @@ -73,8 +73,6 @@ namespace } } -namespace tut -{ namespace { // Shared GL context + LLFontGL bring-up. One per binary. TUT @@ -120,55 +118,47 @@ namespace } }; - typedef test_group llfonttextcache_test; - typedef llfonttextcache_test::object llfonttextcache_object; - tut::llfonttextcache_test llfonttextcache_testcase("LLFontTextCache"); -} // Default construction must not crash and must leave the buffer // in a destructible state. Catches uninitialized-member regressions // that surface as a crash in ~LLFontTextCache trying to free // a garbage list. - template<> template<> - void llfonttextcache_object::test<1>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: a default-constructed cache destroys cleanly", "[LLFontTextCache]") { LLFontTextCache vb; - ensure("default-constructed buffer reaches end-of-scope", true); + CHECK(true); // Implicit destruct at scope exit; assertion above catches any // crash during dtor by being unreached. } // reset() on a freshly-constructed buffer must be a safe no-op. - template<> template<> - void llfonttextcache_object::test<2>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: reset on a fresh cache is safe to call twice", "[LLFontTextCache]") { LLFontTextCache vb; vb.reset(); vb.reset(); // idempotent - ensure("reset on fresh buffer returns cleanly", true); + CHECK(true); } // Static toggles round-trip without affecting later test setup. // The assertions confirm the calls compile and link; observable // round-trip would require accessing the private statics. - template<> template<> - void llfonttextcache_object::test<3>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: enableBufferCollection and enableColorOnlyRegen toggle off and on", "[LLFontTextCache]") { LLFontTextCache::enableBufferCollection(false); LLFontTextCache::enableBufferCollection(true); LLFontTextCache::enableColorOnlyRegen(false); LLFontTextCache::enableColorOnlyRegen(true); - ensure("static toggles set/reset without crash", true); + CHECK(true); } // LLFontTextCache parallel: construct + reset. - template<> template<> - void llfonttextcache_object::test<4>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: a cache resets twice and outlives a buffer collection toggle", "[LLFontTextCache]") { LLFontTextCache wb; wb.reset(); wb.reset(); - ensure("LLFontTextCache construct + reset return cleanly", true); + CHECK(true); LLFontTextCache::enableBufferCollection(false); LLFontTextCache::enableBufferCollection(true); } @@ -176,8 +166,7 @@ namespace // LLFontTextCache constructed alongside LLFontTextCache: // multiple coexisting instances don't share static state in a way // that breaks lifecycle. Pin two buffers in flight at once. - template<> template<> - void llfonttextcache_object::test<5>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: four caches in flight at once each reset cleanly", "[LLFontTextCache]") { LLFontTextCache a; LLFontTextCache b; @@ -187,7 +176,7 @@ namespace b.reset(); w1.reset(); w2.reset(); - ensure("multiple coexisting buffers reset cleanly", true); + CHECK(true); } // =================================================================== @@ -198,54 +187,53 @@ namespace // First call computes; second call with identical params is a cache // hit and returns the same value. Both must equal fontp->getWidthF32. - template<> template<> - void llfonttextcache_object::test<6>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes matches the font and repeats its answer", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache wb; const F32 first = wb.getWidthBytes(font, s, 0, 5, false); const F32 second = wb.getWidthBytes(font, s, 0, 5, false); const F32 ref = font->getWidthF32Bytes(s, 0, 5, false); - ensure("first width matches getWidthF32", first == ref); - ensure("second call same as first (cache hit)", - second == first); - ensure("width is positive", first > 0.f); + UNSCOPED_INFO("first width matches getWidthF32Bytes"); + CHECK(first == ref); + UNSCOPED_INFO("second call same as first (cache hit)"); + CHECK(second == first); + CHECK(first > 0.f); } // Cache miss when geometry differs: changing max_chars between // calls forces recompute. The cache key includes max_chars at // llfonttextcache.cpp:517. - template<> template<> - void llfonttextcache_object::test<7>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes recomputes for a new max_chars and repeats for the same", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache wb; const F32 w_5 = wb.getWidthBytes(font, s, 0, 5, false); const F32 w_3 = wb.getWidthBytes(font, s, 0, 3, false); - ensure("width(5) > width(3)", w_5 > w_3); + CHECK(w_5 > w_3); const F32 w_3_again = wb.getWidthBytes(font, s, 0, 3, false); - ensure_equals("identical max_chars hits cache", w_3_again, w_3); + UNSCOPED_INFO("identical max_chars hits cache"); + CHECK(w_3_again == w_3); } // Cache miss when LLFontGL::sScaleX changes between calls. Pins // mLastScaleX in the cache key (llfonttextcache.cpp:519). - template<> template<> - void llfonttextcache_object::test<8>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes recomputes when LLFontGL::sScaleX changes", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hi"; const F32 saved_scale = LLFontGL::sScaleX; @@ -258,20 +246,19 @@ namespace // Width should scale roughly with sScaleX (within FP rounding // noise). The cache key invalidation is the load-bearing pin. - ensure("scale=0.5 produced different width than scale=1.0", - std::abs(w_1 - w_half) > 0.01f); + UNSCOPED_INFO("scale=0.5 produced different width than scale=1.0"); + CHECK(std::abs(w_1 - w_half) > 0.01f); } // Cache miss when the font's cache generation advances between // calls (a fallback face's atlas mutation, for example). Pins // mLastFontCacheGen at llfonttextcache.cpp:524. - template<> template<> - void llfonttextcache_object::test<9>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes agrees with the font across an atlas generation tick", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache wb; @@ -285,51 +272,51 @@ namespace // answer. Comparing a cache to itself passes whether it invalidated, // recomputed, or returned a stale value -- the only reading that can // fail is one where the buffer and the font disagree. - ensure_equals("width survives an atlas generation tick", - first, second); - ensure_equals("and still agrees with the font it came from", - second, font->getWidthF32Bytes(s, 0, 2, false)); + UNSCOPED_INFO("width survives an atlas generation tick"); + CHECK(first == second); + UNSCOPED_INFO("and still agrees with the font it came from"); + CHECK(second == font->getWidthF32Bytes(s, 0, 2, false)); } // reset() clears the cache — next call recomputes. Pins // llfonttextcache.cpp:480-493. - template<> template<> - void llfonttextcache_object::test<10>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes after reset() still agrees with the font", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache wb; const F32 first = wb.getWidthBytes(font, s, 0, 2, false); wb.reset(); const F32 after = wb.getWidthBytes(font, s, 0, 2, false); - ensure_equals("width survives reset()", first, after); + UNSCOPED_INFO("width survives reset()"); + CHECK(first == after); // Again, against the font: a reset that failed to clear and a reset // that recomputed correctly both return the earlier value, and only // one of those also matches an independent measurement. - ensure_equals("and still agrees with the font it came from", - after, font->getWidthF32Bytes(s, 0, 2, false)); + UNSCOPED_INFO("and still agrees with the font it came from"); + CHECK(after == font->getWidthF32Bytes(s, 0, 2, false)); } // Multiple LLFontTextCache instances stay independent: cache // state in instance A doesn't affect instance B. - template<> template<> - void llfonttextcache_object::test<11>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: one cache's reset leaves another's width unchanged", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache a, b; const F32 wa = a.getWidthBytes(font, s, 0, 2, false); a.reset(); const F32 wb_val = b.getWidthBytes(font, s, 0, 2, false); - ensure_equals("instance B unaffected by A's reset", wa, wb_val); + UNSCOPED_INFO("instance B unaffected by A's reset"); + CHECK(wa == wb_val); } // The bound the cache is keyed on, over text where a byte is not a @@ -337,18 +324,17 @@ namespace // count, a character count and a glyph count are the same number, so a // cache keyed in the wrong one of them hits and misses identically on all // of them and every assertion still passes. - template<> template<> - void llfonttextcache_object::test<12>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: getWidthBytes keys its cache on bytes over multi-byte text", "[LLFontTextCache]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); // "A", a three-byte character, "B": five bytes, three characters. const std::string s = "A\xE6\x97\xA5" "B"; if (font->getWidthF32Bytes(s, 0, (S32)s.size(), false) <= 0.f) - skip("font produced no width for the sample"); + SKIP("font produced no width for the sample"); // Every bound, boundary or not, has to agree with the font. A bound of // 1 and a bound of 4 are one character apart and three bytes apart; @@ -357,18 +343,20 @@ namespace LLFontTextCache wb; for (S32 bound : { 0, 1, 4, 5 }) { + CAPTURE(bound); const F32 cached = wb.getWidthBytes(font, s, 0, bound, false); - ensure_equals("cached width agrees with the font", - cached, font->getWidthF32Bytes(s, 0, bound, false)); + UNSCOPED_INFO("cached width agrees with the font"); + REQUIRE(cached == font->getWidthF32Bytes(s, 0, bound, false)); // And again, now that it is cached. - ensure_equals("and still agrees on the second reading", - wb.getWidthBytes(font, s, 0, bound, false), cached); + UNSCOPED_INFO("and still agrees on the second reading"); + REQUIRE(wb.getWidthBytes(font, s, 0, bound, false) == cached); } // Distinct bounds must not collapse onto one entry. const F32 w1 = wb.getWidthBytes(font, s, 0, 1, false); const F32 w4 = wb.getWidthBytes(font, s, 0, 4, false); - ensure("one character in is narrower than two", w1 < w4); + UNSCOPED_INFO("one character in is narrower than two"); + CHECK(w1 < w4); } // The guard that says a cache is being asked about the text it was named @@ -381,38 +369,37 @@ namespace // at one end or the other. Two long strings differing only in the middle // read as the same text, deliberately, and this is the whole of what that // bound gives up. - template<> template<> - void llfonttextcache_object::test<13>() + TEST_CASE_METHOD(llfonttextcache_data, "LLFontTextCache: ALFontCacheKey tells recorded text apart by length and both ends", "[LLFontTextCache]") { ALFontCacheKey key; - ensure("the first text asked about is the one recorded", - key.sameTextAsRecorded("Inventory")); - ensure("and reads as the same text when asked again", - key.sameTextAsRecorded("Inventory")); + UNSCOPED_INFO("the first text asked about is the one recorded"); + REQUIRE(key.sameTextAsRecorded("Inventory")); + UNSCOPED_INFO("and reads as the same text when asked again"); + CHECK(key.sameTextAsRecorded("Inventory")); // What a password field does: one bullet per character, three bytes // apiece, over text that is one byte per character. - ensure("a substituted string is not the text behind it", - !key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); + UNSCOPED_INFO("a substituted string is not the text behind it"); + CHECK_FALSE(key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); // Same length, differing at the front and at the back -- the second is // what a sample taken only from the front would miss. ALFontCacheKey front; - ensure("records", front.sameTextAsRecorded("Received Items")); - ensure("a different string of the same length is caught", - !front.sameTextAsRecorded("Xeceived Items")); + REQUIRE(front.sameTextAsRecorded("Received Items")); + UNSCOPED_INFO("a different string of the same length is caught"); + CHECK_FALSE(front.sameTextAsRecorded("Xeceived Items")); ALFontCacheKey back; const std::string common(64, 'a'); - ensure("records", back.sameTextAsRecorded(common + "tail")); - ensure("a difference past the sampled front is caught", - !back.sameTextAsRecorded(common + "tai1")); + REQUIRE(back.sameTextAsRecorded(common + "tail")); + UNSCOPED_INFO("a difference past the sampled front is caught"); + CHECK_FALSE(back.sameTextAsRecorded(common + "tai1")); // Naming a new source is what clears the recorded text, so the next // string asked about becomes the one this is holding. key.forgetSource(); - ensure("a forgotten source records whatever is asked next", - key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); + UNSCOPED_INFO("a forgotten source records whatever is asked next"); + CHECK(key.sameTextAsRecorded("\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2")); } // =================================================================== @@ -422,8 +409,6 @@ namespace // framebuffer. // =================================================================== -namespace -{ struct llfonttextcache_render_data { ll_test::HeadlessGL& gl = ll_test::sharedHeadlessGL(); @@ -441,21 +426,16 @@ namespace } }; - typedef test_group llfonttextcache_render_test; - typedef llfonttextcache_render_test::object llfonttextcache_render_object; - tut::llfonttextcache_render_test llfonttextcache_render_testcase("LLFontTextCacheRender"); -} // First render() populates the buffer cache and returns the // character count rendered. Pins genBuffers + drawBuffer end-to-end // through the stub UI shader. - template<> template<> - void llfonttextcache_render_object::test<1>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: renderBytes on a fresh cache returns the bytes it drew", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; gl.clearFramebuffer(); @@ -466,7 +446,7 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, /*max_chars=*/5); - ensure_equals("render returned 5 chars", n, 5); + CHECK(n == 5); } // Color-only fast path (b317778477): same render twice with same @@ -474,13 +454,12 @@ namespace // recolorBuffers — no genBuffers, no atlas alloc. The global // generation counter must NOT advance between the first and second // calls (atlas already populated; recolor doesn't allocate). - template<> template<> - void llfonttextcache_render_object::test<2>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a repeated identical render allocates nothing in the atlas", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache vb; @@ -499,8 +478,8 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 5); const S32 gen_after = LLFontBitmapCache::getGlobalGeneration(); - ensure_equals("no atlas alloc on repeat render (gen counter stable)", - gen_after, gen_before); + UNSCOPED_INFO("no atlas alloc on repeat render (gen counter stable)"); + CHECK(gen_after == gen_before); } // Color change with same geometry: takes the recolor fast path — @@ -508,13 +487,12 @@ namespace // attribute, no genBuffers. Pins llfonttextcache.cpp:203-210. // Observable: global generation counter doesn't advance (no atlas // mutation), and the call returns the same character count. - template<> template<> - void llfonttextcache_render_object::test<3>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a color change alone keeps the count and allocates nothing", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hello"; LLFontTextCache vb; @@ -527,10 +505,10 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 5); const S32 gen_after = LLFontBitmapCache::getGlobalGeneration(); - ensure_equals("color change preserves char count", - n_red, n_white); - ensure_equals("recolor fast path: no atlas mutation", - gen_after, gen_before); + UNSCOPED_INFO("color change preserves char count"); + CHECK(n_red == n_white); + UNSCOPED_INFO("recolor fast path: no atlas mutation"); + CHECK(gen_after == gen_before); } // Geometry change forces full regen: render at x=100, then at @@ -538,13 +516,12 @@ namespace // shouldn't advance (nothing about geometry change touches the // atlas), but the buffer cache had to rebuild. The post-render // render-target reflects the second position. - template<> template<> - void llfonttextcache_render_object::test<4>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a render moved to another x returns the same count", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "AB"; LLFontTextCache vb; @@ -556,21 +533,21 @@ namespace const S32 n2 = vb.renderBytes(font, s, 0, 200.f, 100.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); - ensure_equals("char count stable after geometry change", n1, n2); - ensure("both calls returned positive count", n1 > 0); + UNSCOPED_INFO("char count stable after geometry change"); + CHECK(n1 == n2); + CHECK(n1 > 0); } // enableColorOnlyRegen(false) disables the fast path entirely: // a same-geometry color-change call falls through to genBuffers // (full regen). The static toggle pins the flag-controlled branch // at llfonttextcache.cpp:210. Restore the flag in dtor. - template<> template<> - void llfonttextcache_render_object::test<5>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: with enableColorOnlyRegen off a color change still draws every byte", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache vb; @@ -586,19 +563,19 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, 2); LLFontTextCache::enableColorOnlyRegen(true); - ensure_equals("render with fast path disabled still returns 2", n, 2); + UNSCOPED_INFO("render with fast path disabled still returns 2"); + CHECK(n == 2); } // Style flip NORMAL → BOLD forces full regen — different glyph // index stream. Pins 00f3a4e93d (style invalidation in geometry- // invalid check at llfonttextcache.cpp:175). - template<> template<> - void llfonttextcache_render_object::test<6>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a style flip from NORMAL to BOLD keeps the count", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); const std::string s = "Hi"; LLFontTextCache vb; @@ -609,8 +586,9 @@ namespace const S32 n_bold = vb.renderBytes(font, s, 0, 100.f, 100.f, LLColor4::white, LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::BOLD, LLFontGL::NO_SHADOW, 2); - ensure_equals("char count stable across style flip", n_norm, n_bold); - ensure("both renders returned positive", n_norm > 0); + UNSCOPED_INFO("char count stable across style flip"); + CHECK(n_norm == n_bold); + CHECK(n_norm > 0); } // Truncated DROP_SHADOW_SOFT render with use_ellipses=true: the @@ -619,13 +597,12 @@ namespace // outer's on_pass_boundary callback and routed its shadow batch // into mForegroundBufferList — visible as off-color shadow tinting // in the recolor fast path. Fix renders the ellipsis with NO_SHADOW. - template<> template<> - void llfonttextcache_render_object::test<7>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: an ellipsized DROP_SHADOW_SOFT render adds no shadow quads for the dots", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -648,8 +625,11 @@ namespace /*right_x=*/nullptr, /*use_ellipses=*/true, /*use_color=*/true); - ensure("truncation actually happened (n < full string)", - n > 0 && n < (S32)s.length()); + { + INFO("truncation actually happened (n < full string)"); + CHECK(n > 0); + REQUIRE(n < (S32)s.length()); + } const auto counts = ll_test::VertexBufferProbe::count(vb); @@ -657,25 +637,24 @@ namespace // NOT add to this list — the ellipsis render is single-pass // (NO_SHADOW) post-fix. const size_t expected_shadow = static_cast(n) * 5u; - ensure_equals("shadow list = visible chars * 5 (ellipsis adds 0)", - counts.shadow_quads, expected_shadow); + UNSCOPED_INFO("shadow list = visible chars * 5 (ellipsis adds 0)"); + CHECK(counts.shadow_quads == expected_shadow); // Foreground: 1 quad per visible char + 3 ellipsis chars. const size_t expected_fg = static_cast(n) + 3u; - ensure_equals("foreground list = visible + 3 ellipsis foreground", - counts.foreground_quads, expected_fg); + UNSCOPED_INFO("foreground list = visible + 3 ellipsis foreground"); + CHECK(counts.foreground_quads == expected_fg); } // Sanity check on chars_drawn: when use_ellipses=true triggers // truncation, render's return value reports only the visible // pre-ellipsis character count, not including the appended "...". - template<> template<> - void llfonttextcache_render_object::test<8>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: an ellipsized render counts the visible bytes and not the dots", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -693,8 +672,11 @@ namespace nullptr, /*use_ellipses=*/true, true); - ensure("truncated count is positive but less than full string", - n_truncated > 0 && n_truncated < (S32)s.length()); + { + INFO("truncated count is positive but less than full string"); + CHECK(n_truncated > 0); + REQUIRE(n_truncated < (S32)s.length()); + } // Same render with max_pixels wide enough to fit the whole // string. No truncation, no ellipsis path; full count returned. @@ -709,8 +691,8 @@ namespace nullptr, /*use_ellipses=*/true, true); - ensure_equals("full-fit render returns the entire string length", - n_full, (S32)s.length()); + UNSCOPED_INFO("full-fit render returns the entire string length"); + CHECK(n_full == (S32)s.length()); } // Moving text does not rebuild it. Glyph geometry is built relative to the @@ -718,13 +700,12 @@ namespace // floater drag or a resize replays the capture under a new transform. That // was not true while the origin was added into the vertices: everything // visible in a moving container was reshaped every frame. - template<> template<> - void llfonttextcache_render_object::test<9>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: moving text does not rebuild it", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; @@ -777,13 +758,15 @@ namespace LLFontGL::sCurOrigin = saved_origin; - ensure("the first draw builds", after_settle > before); + UNSCOPED_INFO("the first draw builds"); + CHECK(after_settle > before); // And without this it would hold for a cache that never replays at all. - ensure("an unmoved redraw eventually replays", settled); - ensure_equals("moving the text replays rather than rebuilds", - after_moves, after_settle); - ensure("a draw somewhere else still rebuilds", - after_real_change > after_moves); + UNSCOPED_INFO("an unmoved redraw eventually replays"); + CHECK(settled); + UNSCOPED_INFO("moving the text replays rather than rebuilds"); + CHECK(after_moves == after_settle); + UNSCOPED_INFO("a draw somewhere else still rebuilds"); + CHECK(after_real_change > after_moves); } // Text that shapes to no geometry settles too. A cell with nothing in it @@ -792,13 +775,12 @@ namespace // and a scroll list draws a cell per column whether or not it holds // anything. Such a piece of text reshaped itself on every frame it stayed // on screen, and the count is the only place it showed. - template<> template<> - void llfonttextcache_render_object::test<10>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: text that shapes to no geometry stops rebuilding too", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; font->generateASCIIglyphs(); @@ -822,7 +804,8 @@ namespace const std::string nothing; LLFontTextCache blank; - ensure("an empty cell stops rebuilding", settle([&]() + UNSCOPED_INFO("an empty cell stops rebuilding"); + CHECK(settle([&]() { blank.setSource(¬hing, 0); blank.renderBytes(font, nothing, 0, 12.f, 34.f, LLColor4::white, @@ -832,7 +815,8 @@ namespace const std::string clipped = "Nothing of this fits"; LLFontTextCache narrow; - ensure("text with no room to draw stops rebuilding", settle([&]() + UNSCOPED_INFO("text with no room to draw stops rebuilding"); + CHECK(settle([&]() { narrow.setSource(&clipped, 0); narrow.renderBytes(font, clipped, 0, 12.f, 34.f, LLColor4::white, @@ -849,8 +833,8 @@ namespace LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::NO_SHADOW, S32_MAX, /*max_pixels=*/2000); - ensure("room to draw rebuilds", - LLFontTextCache::regenCount() > before_visible); + UNSCOPED_INFO("room to draw rebuilds"); + CHECK(LLFontTextCache::regenCount() > before_visible); } // A width and a draw come off the same glyphs, so the state the font @@ -860,13 +844,12 @@ namespace // box, the ellipsis fit, the auto-resize. Without the other half being // dropped alongside, the draw was told nothing had moved and replayed a // capture whose glyphs had been rebuilt underneath it. - template<> template<> - void llfonttextcache_render_object::test<11>() + TEST_CASE_METHOD(llfonttextcache_render_data, "LLFontTextCacheRender: a resolution change a measurement saw still rebuilds the draw", "[LLFontTextCacheRender]") { if (!fileExists(kFontsXml)) - skip("fonts.xml not present in test data dir"); + SKIP("fonts.xml not present in test data dir"); LLFontGL* font = LLFontGL::getFontSansSerif(); - ensure("font available", font != nullptr); + REQUIRE(font != nullptr); ll_test::FontStateScope scope; font->generateASCIIglyphs(); @@ -895,7 +878,8 @@ namespace draw(); settled = (LLFontTextCache::regenCount() == before_draw); } - ensure("measure then draw eventually replays", settled); + UNSCOPED_INFO("measure then draw eventually replays"); + CHECK(settled); const S32 saved_res_gen = LLFontGL::sResolutionGeneration; ++LLFontGL::sResolutionGeneration; @@ -908,7 +892,7 @@ namespace LLFontGL::sResolutionGeneration = saved_res_gen; - ensure("a draw after a measurement saw the same change rebuilds", - after_stale_draw > before_stale_draw); + UNSCOPED_INFO("a draw after a measurement saw the same change rebuilds"); + CHECK(after_stale_draw > before_stale_draw); } } diff --git a/indra/llrender/tests/llglslshader_gl_test.cpp b/indra/llrender/tests/llglslshader_gl_test.cpp index fddfbe5d20..dea89e7cf8 100644 --- a/indra/llrender/tests/llglslshader_gl_test.cpp +++ b/indra/llrender/tests/llglslshader_gl_test.cpp @@ -42,7 +42,7 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include @@ -53,10 +53,8 @@ #include #include -namespace tut +namespace { - namespace - { const char* kVertex = "#version 330\n" "void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }\n"; @@ -643,14 +641,14 @@ namespace tut // builds a program. void setUpShaders() { - ensure_equals("the test's names follow the product's", mShaders.mReservedUniforms.size(), - size_t(LLShaderMgr::END_RESERVED_UNIFORMS)); + UNSCOPED_INFO("the test's names follow the product's"); + REQUIRE(mShaders.mReservedUniforms.size() == size_t(LLShaderMgr::END_RESERVED_UNIFORMS)); for (const char* name : kTestReservedNames) { mShaders.mReservedUniforms.push_back(name); } - ensure_equals("the test's blocks follow the product's", mShaders.mReservedStorageBlocks.size(), - size_t(LLShaderMgr::END_RESERVED_STORAGE_BLOCKS)); + UNSCOPED_INFO("the test's blocks follow the product's"); + REQUIRE(mShaders.mReservedStorageBlocks.size() == size_t(LLShaderMgr::END_RESERVED_STORAGE_BLOCKS)); for (const auto& block : kTestStorageBlocks) { mShaders.mReservedStorageBlocks.push_back({ block.first, block.second }); @@ -658,8 +656,8 @@ namespace tut for (const auto& object : kStubObjects) { S32 level = 1; - ensure(std::string("compiled ") + object.first, - mShaders.loadShaderFile(object.first, level, object.second) != 0); + CAPTURE(object.first); + REQUIRE(mShaders.loadShaderFile(object.first, level, object.second) != 0); } } @@ -668,7 +666,7 @@ namespace tut { if (!ll_test::contextHasCompute()) { - skip("the context has no compute"); + SKIP("the context has no compute"); } LLRender::sCompute = true; setUpShaders(); @@ -685,7 +683,11 @@ namespace tut const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mShader.mProgramObject = glCreateProgram(); mShader.mName = "uniform cache (test)"; mShader.attachObject(vs); @@ -693,8 +695,8 @@ namespace tut const bool linked = mShader.mapAttributes(); glDeleteShader(vs); glDeleteShader(fs); - ensure("program links", linked); - ensure("program maps", mShader.mapUniforms()); + REQUIRE(linked); + REQUIRE(mShader.mapUniforms()); mShader.bind(); } @@ -732,40 +734,45 @@ namespace tut // What the program holds at a uniform, as GL reads it back, against what a check expects of its first // components. Floats are compared by their bits, so a check is exact. - void ensureInts(const std::string& what, const char* name, std::initializer_list expected) const + void require_ints(const std::string& what, const char* name, std::initializer_list expected) const { + INFO(what); std::array held = {}; glGetUniformiv(mShader.mProgramObject, location(name), held.data()); - size_t i = 0; + size_t component = 0; for (const GLint value : expected) { - ensure_equals(what + ", component " + std::to_string(i), held[i], value); - ++i; + CAPTURE(component); + REQUIRE(held[component] == value); + ++component; } } - void ensureUints(const std::string& what, const char* name, std::initializer_list expected) const + void require_uints(const std::string& what, const char* name, std::initializer_list expected) const { + INFO(what); std::array held = {}; glGetUniformuiv(mShader.mProgramObject, location(name), held.data()); - size_t i = 0; + size_t component = 0; for (const GLuint value : expected) { - ensure_equals(what + ", component " + std::to_string(i), held[i], value); - ++i; + CAPTURE(component); + REQUIRE(held[component] == value); + ++component; } } - void ensureFloats(const std::string& what, const char* name, std::initializer_list expected) const + void require_floats(const std::string& what, const char* name, std::initializer_list expected) const { + INFO(what); std::array held = {}; glGetUniformfv(mShader.mProgramObject, location(name), held.data()); - size_t i = 0; + size_t component = 0; for (const F32 value : expected) { - ensure_equals(what + ", component " + std::to_string(i) + " bits", - std::bit_cast(held[i]), std::bit_cast(value)); - ++i; + CAPTURE(component); + REQUIRE(std::bit_cast(held[component]) == std::bit_cast(value)); + ++component; } } @@ -784,262 +791,251 @@ namespace tut LLPointer mUintImage; }; - typedef test_group llglslshader_t; - typedef llglslshader_t::object llglslshader_object_t; - tut::llglslshader_t tut_llglslshader("llglslshader_gl"); - } // Ints and uints arrive exactly past 2^24: each set below differs from the one before only in values that // convert to the same float, and only in some of its components. - template<> template<> - void llglslshader_object_t::test<1>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: ints and uints arrive exactly past 2^24", "[llglslshader_gl]") { setUp(); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, BIG); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, BIG + 1); - ensureInts("int", "light_count", { BIG + 1 }); + require_ints("int", "light_count", { BIG + 1 }); const GLint first[4] = { BIG, -BIG, BIG, BIG }; const GLint second[4] = { BIG, -BIG, BIG, BIG + 1 }; const GLint third[4] = { BIG + 1, -BIG - 1, BIG, BIG + 1 }; mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, first); - ensureInts("ivec4", "sun_up_factor", { BIG, -BIG, BIG, BIG }); + require_ints("ivec4", "sun_up_factor", { BIG, -BIG, BIG, BIG }); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, second); - ensureInts("ivec4, w alone changed", "sun_up_factor", { BIG, -BIG, BIG, BIG + 1 }); + require_ints("ivec4, w alone changed", "sun_up_factor", { BIG, -BIG, BIG, BIG + 1 }); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, third); - ensureInts("ivec4, x and y changed", "sun_up_factor", { BIG + 1, -BIG - 1, BIG, BIG + 1 }); + require_ints("ivec4, x and y changed", "sun_up_factor", { BIG + 1, -BIG - 1, BIG, BIG + 1 }); mShader.uniform1ui(LLShaderMgr::FRAME_ID, (GLuint)BIG); mShader.uniform1ui(LLShaderMgr::FRAME_ID, (GLuint)BIG + 1); - ensureUints("uint", "uFrameId", { (GLuint)BIG + 1 }); + require_uints("uint", "uFrameId", { (GLuint)BIG + 1 }); // The sharpening constants are floats' bits. 1.0's and the next float up's are both 1065353216 as floats. const GLuint one[4] = { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800000u }; const GLuint next[4] = { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }; mShader.uniform4uiv(LLShaderMgr::CAS_PARAM_0, 1, one); mShader.uniform4uiv(LLShaderMgr::CAS_PARAM_0, 1, next); - ensureUints("uvec4, w alone changed", "cas_param_0", { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }); + require_uints("uvec4, w alone changed", "cas_param_0", { 0x3F800000u, 0x3F800000u, 0x3F800000u, 0x3F800001u }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A, then B, then A again holds A, for every kind of set: each upload replaces what its location's slot holds, // so the third set is compared with B. The pairs mix a setter's value and array forms, which share a kind. - template<> template<> - void llglslshader_object_t::test<2>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: setting A then B then A again holds A for every uniform type", "[llglslshader_gl]") { setUp(); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 0.5f); - ensureFloats("vec4 B", "color", { 0.25f, 0.5f, 0.75f, 0.5f }); + require_floats("vec4 B", "color", { 0.25f, 0.5f, 0.75f, 0.5f }); const F32 color[4] = { 0.25f, 0.5f, 0.75f, 1.f }; mShader.uniform4fv(LLShaderMgr::DIFFUSE_COLOR, 1, color); - ensureFloats("vec4 A again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("vec4 A again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.uniform3f(LLShaderMgr::DEFERRED_SUN_DIR, 0.f, 0.5f, 1.f); const F32 sun[3] = { 0.f, 0.5f, 0.25f }; mShader.uniform3fv(LLShaderMgr::DEFERRED_SUN_DIR, 1, sun); - ensureFloats("vec3 B", "sun_dir", { 0.f, 0.5f, 0.25f }); + require_floats("vec3 B", "sun_dir", { 0.f, 0.5f, 0.25f }); mShader.uniform3f(LLShaderMgr::DEFERRED_SUN_DIR, 0.f, 0.5f, 1.f); - ensureFloats("vec3 A again", "sun_dir", { 0.f, 0.5f, 1.f }); + require_floats("vec3 A again", "sun_dir", { 0.f, 0.5f, 1.f }); mShader.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, 640.f, 480.f); const F32 res[2] = { 640.f, 360.f }; mShader.uniform2fv(LLShaderMgr::DEFERRED_SCREEN_RES, 1, res); - ensureFloats("vec2 B", "screen_res", { 640.f, 360.f }); + require_floats("vec2 B", "screen_res", { 640.f, 360.f }); mShader.uniform2f(LLShaderMgr::DEFERRED_SCREEN_RES, 640.f, 480.f); - ensureFloats("vec2 A again", "screen_res", { 640.f, 480.f }); + require_floats("vec2 A again", "screen_res", { 640.f, 480.f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); const F32 intensity = 0.5f; mShader.uniform1fv(LLShaderMgr::ENVIRONMENT_INTENSITY, 1, &intensity); - ensureFloats("float B", "env_intensity", { 0.5f }); + require_floats("float B", "env_intensity", { 0.5f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); - ensureFloats("float A again", "env_intensity", { 0.25f }); + require_floats("float A again", "env_intensity", { 0.25f }); const GLint ivec_a[4] = { 1, 2, 3, 4 }; const GLint ivec_b[4] = { 1, 2, 3, 5 }; mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_a); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_b); mShader.uniform4iv(LLShaderMgr::SUN_UP_FACTOR, 1, ivec_a); - ensureInts("ivec4 A again", "sun_up_factor", { 1, 2, 3, 4 }); + require_ints("ivec4 A again", "sun_up_factor", { 1, 2, 3, 4 }); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); const GLint count = 4; mShader.uniform1iv(LLShaderMgr::MULTI_LIGHT_COUNT, 1, &count); - ensureInts("int B", "light_count", { 4 }); + require_ints("int B", "light_count", { 4 }); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); - ensureInts("int A again", "light_count", { 3 }); + require_ints("int A again", "light_count", { 3 }); mShader.uniform1ui(LLShaderMgr::FRAME_ID, 7u); mShader.uniform1ui(LLShaderMgr::FRAME_ID, 8u); mShader.uniform1ui(LLShaderMgr::FRAME_ID, 7u); - ensureUints("uint A again", "uFrameId", { 7u }); + require_uints("uint A again", "uFrameId", { 7u }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A float set twice holds its value, and a new value replaces it. Raw uploads behind the cache's back show // which sets reached GL: just after a link the cache holds nothing, not a zero, so a set of 0 uploads; and a set // of the value last uploaded does not. - template<> template<> - void llglslshader_object_t::test<3>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a float set skips GL only when it repeats the last value uploaded", "[llglslshader_gl]") { setUp(); const GLint loc = location("env_intensity"); - ensure("env_intensity is active", loc >= 0); + UNSCOPED_INFO("env_intensity is active"); + CHECK(loc >= 0); glUniform1f(loc, 5.f); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.f); - ensureFloats("the first set uploads, a zero too", "env_intensity", { 0.f }); + require_floats("the first set uploads, a zero too", "env_intensity", { 0.f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); - ensureFloats("set twice", "env_intensity", { 0.5f }); + require_floats("set twice", "env_intensity", { 0.5f }); glUniform1f(loc, 2.f); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); - ensureFloats("the value last uploaded is not uploaded again", "env_intensity", { 2.f }); + require_floats("the value last uploaded is not uploaded again", "env_intensity", { 2.f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.75f); - ensureFloats("a new value replaces it", "env_intensity", { 0.75f }); + require_floats("a new value replaces it", "env_intensity", { 0.75f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // LLGLSLShader::link forgets the cache: the relink resets every uniform to 0, and the value set before it, // set again, arrives. - template<> template<> - void llglslshader_object_t::test<4>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: LLGLSLShader::link resets uniforms to 0 and forgets the cache", "[llglslshader_gl]") { setUp(); const GLint before = location("color"); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); - ensureFloats("set before the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("set before the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensure("program relinks", mShader.link()); + CHECK(mShader.link()); if (location("color") != before) { - skip("the relink moved the uniform, so a stale slot would not be consulted"); + SKIP("the relink moved the uniform, so a stale slot would not be consulted"); } - ensureFloats("the relink reset it", "color", { 0.f, 0.f, 0.f, 0.f }); + require_floats("the relink reset it", "color", { 0.f, 0.f, 0.f, 0.f }); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); - ensureFloats("set again after the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("set again after the relink", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // mapUniforms forgets the cache, and setMinimumAlpha's own: a new program, linked by GL alone and mapped, holds // the values set again after it, though they are the ones set on the program it replaced. - template<> template<> - void llglslshader_object_t::test<5>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: mapUniforms forgets the uniform cache and setMinimumAlpha's own", "[llglslshader_gl]") { setUp(); const GLint before = location("color"); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); mShader.setMinimumAlpha(0.5f); - ensureFloats("colour on the old program", "color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensureFloats("minimum alpha on the old program", "minimum_alpha", { 0.5f }); + require_floats("colour on the old program", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("minimum alpha on the old program", "minimum_alpha", { 0.5f }); const GLuint fresh = linkRaw(); - ensure("the new program links", fresh != 0); + UNSCOPED_INFO("the new program links"); + REQUIRE(fresh != 0); LLGLSLShader::unbind(); const GLuint old = mShader.mProgramObject; mShader.mProgramObject = fresh; forget_program_samplers(old); glDeleteProgram(old); - ensure("the new program maps", mShader.mapUniforms()); + CHECK(mShader.mapUniforms()); mShader.bind(); if (location("color") != before) { - skip("the new program put the uniform elsewhere, so a stale slot would not be consulted"); + SKIP("the new program put the uniform elsewhere, so a stale slot would not be consulted"); } - ensureFloats("the new program starts at 0", "color", { 0.f, 0.f, 0.f, 0.f }); + require_floats("the new program starts at 0", "color", { 0.f, 0.f, 0.f, 0.f }); mShader.uniform4f(LLShaderMgr::DIFFUSE_COLOR, 0.25f, 0.5f, 0.75f, 1.f); - ensureFloats("colour set again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("colour set again", "color", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.setMinimumAlpha(0.5f); - ensureFloats("minimum alpha set again", "minimum_alpha", { 0.5f }); + require_floats("minimum alpha set again", "minimum_alpha", { 0.5f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A bool takes the int setters and the float ones, and holds whichever set it last, the int set after a float // one included. INT_MIN and -0.0 share their bits: true as an int, false as a float, so only the kind a slot // records tells the float set from the int one before it. - template<> template<> - void llglslshader_object_t::test<6>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a bool takes int and float setters and holds whichever set it last", "[llglslshader_gl]") { setUp(); mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, 1); - ensureInts("true by int", "uGrainAnimate", { 1 }); + require_ints("true by int", "uGrainAnimate", { 1 }); mShader.uniform1f(LLShaderMgr::GRAIN_ANIMATE, 0.f); - ensureInts("false by float", "uGrainAnimate", { 0 }); + require_ints("false by float", "uGrainAnimate", { 0 }); mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, 1); - ensureInts("true by int again", "uGrainAnimate", { 1 }); + require_ints("true by int again", "uGrainAnimate", { 1 }); // Read at run time, so nothing folds the sign of the zero away. volatile U32 sign_bit = 0x80000000u; const F32 negative_zero = std::bit_cast(static_cast(sign_bit)); mShader.uniform1i(LLShaderMgr::GRAIN_ANIMATE, INT_MIN); - ensureInts("true by INT_MIN", "uGrainAnimate", { 1 }); + require_ints("true by INT_MIN", "uGrainAnimate", { 1 }); mShader.uniform1f(LLShaderMgr::GRAIN_ANIMATE, negative_zero); - ensureInts("false by -0.0, the same bits", "uGrainAnimate", { 0 }); + require_ints("false by -0.0, the same bits", "uGrainAnimate", { 0 }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The reserved index and the name of one uniform share its slot: A by index, B by name, then A by index holds A. - template<> template<> - void llglslshader_object_t::test<7>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: the reserved index and the name of one uniform share its cache slot", "[llglslshader_gl]") { setUp(); const LLStaticHashedString light_count("light_count"); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); mShader.uniform1i(light_count, 4); - ensureInts("by name", "light_count", { 4 }); + require_ints("by name", "light_count", { 4 }); mShader.uniform1i(LLShaderMgr::MULTI_LIGHT_COUNT, 3); - ensureInts("by index after the name", "light_count", { 3 }); + require_ints("by index after the name", "light_count", { 3 }); mShader.uniform1i(light_count, 4); - ensureInts("by name after the index", "light_count", { 4 }); + require_ints("by name after the index", "light_count", { 4 }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The fast setters upload without comparing and leave the cache describing what they uploaded: a cached set of // the value before a fast one arrives. - template<> template<> - void llglslshader_object_t::test<8>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: fast setters upload unconditionally and leave the cache matching GL", "[llglslshader_gl]") { setUp(); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); mShader.fastUniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.25f); - ensureFloats("fast float", "env_intensity", { 0.25f }); + require_floats("fast float", "env_intensity", { 0.25f }); mShader.uniform1f(LLShaderMgr::ENVIRONMENT_INTENSITY, 0.5f); - ensureFloats("cached float after the fast one", "env_intensity", { 0.5f }); + require_floats("cached float after the fast one", "env_intensity", { 0.5f }); const F32 a[4] = { 0.25f, 0.5f, 0.75f, 1.f }; const F32 b[4] = { 1.f, 0.75f, 0.5f, 0.25f }; mShader.uniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, a); mShader.fastUniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, b); - ensureFloats("fast vec4", "specular_color", { 1.f, 0.75f, 0.5f, 0.25f }); + require_floats("fast vec4", "specular_color", { 1.f, 0.75f, 0.5f, 0.25f }); mShader.uniform4fv(LLShaderMgr::SPECULAR_COLOR, 1, a); - ensureFloats("cached vec4 after the fast one", "specular_color", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("cached vec4 after the fast one", "specular_color", { 0.25f, 0.5f, 0.75f, 1.f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An array set uploads every time and forgets what its location held: element 0 set alone, then the array over // it, then element 0 alone again holds element 0's value; and an array whose element 0 is unchanged still // reaches GL. - template<> template<> - void llglslshader_object_t::test<9>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: an array set uploads every time and forgets what its location held", "[llglslshader_gl]") { setUp(); @@ -1048,74 +1044,77 @@ namespace tut const F32 two_again[8] = { 1.f, 1.f, 1.f, 1.f, 0.25f, 0.25f, 0.25f, 0.25f }; mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 1, single); - ensureFloats("element 0 alone", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("element 0 alone", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two); - ensureFloats("element 0 by the array", "light_diffuse[0]", { 1.f, 1.f, 1.f, 1.f }); - ensureFloats("element 1 by the array", "light_diffuse[1]", { 0.5f, 0.5f, 0.5f, 0.5f }); + require_floats("element 0 by the array", "light_diffuse[0]", { 1.f, 1.f, 1.f, 1.f }); + require_floats("element 1 by the array", "light_diffuse[1]", { 0.5f, 0.5f, 0.5f, 0.5f }); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 1, single); - ensureFloats("element 0 alone again", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); + require_floats("element 0 alone again", "light_diffuse[0]", { 0.25f, 0.5f, 0.75f, 1.f }); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two); mShader.uniform4fv(LLShaderMgr::LIGHT_DIFFUSE, 2, two_again); - ensureFloats("element 1 by an array whose element 0 did not change", "light_diffuse[1]", - { 0.25f, 0.25f, 0.25f, 0.25f }); + require_floats("element 1 by an array whose element 0 did not change", "light_diffuse[1]", + { 0.25f, 0.25f, 0.25f, 0.25f }); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Where the context has compute, its limits are at least what GL 4.3 promises, which is what the features size // themselves by. - template<> template<> - void llglslshader_object_t::test<10>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: compute limits are at least what GL 4.3 promises", "[llglslshader_gl]") { if (!gGLManager.mHasCompute) { - skip("the context has no compute"); + SKIP("the context has no compute"); } for (U32 i = 0; i < 3; ++i) { - ensure("work group count " + std::to_string(i), gGLManager.mMaxComputeWorkGroupCount[i] >= 65535); + CAPTURE(i); + REQUIRE(gGLManager.mMaxComputeWorkGroupCount[i] >= 65535); } - ensure("work group size x", gGLManager.mMaxComputeWorkGroupSize[0] >= 1024); - ensure("work group size y", gGLManager.mMaxComputeWorkGroupSize[1] >= 1024); - ensure("work group size z", gGLManager.mMaxComputeWorkGroupSize[2] >= 64); - ensure("invocations", gGLManager.mMaxComputeWorkGroupInvocations >= 1024); - ensure("shared memory", gGLManager.mMaxComputeSharedMemorySize >= 32768); - ensure("image units", gGLManager.mMaxImageUnits >= 8); - ensure("compute storage blocks", gGLManager.mMaxComputeShaderStorageBlocks >= 8); - ensure("storage bindings", gGLManager.mMaxShaderStorageBufferBindings >= 8); - ensure("storage block size", gGLManager.mMaxShaderStorageBlockSize >= (S64(1) << 27)); + CHECK(gGLManager.mMaxComputeWorkGroupSize[0] >= 1024); + CHECK(gGLManager.mMaxComputeWorkGroupSize[1] >= 1024); + CHECK(gGLManager.mMaxComputeWorkGroupSize[2] >= 64); + CHECK(gGLManager.mMaxComputeWorkGroupInvocations >= 1024); + CHECK(gGLManager.mMaxComputeSharedMemorySize >= 32768); + CHECK(gGLManager.mMaxImageUnits >= 8); + CHECK(gGLManager.mMaxComputeShaderStorageBlocks >= 8); + CHECK(gGLManager.mMaxShaderStorageBufferBindings >= 8); + CHECK(gGLManager.mMaxShaderStorageBlockSize >= (S64(1) << 27)); const S32 alignment = gGLManager.mShaderStorageBufferOffsetAlignment; - ensure("storage offset alignment a power of two", alignment > 0 && (alignment & (alignment - 1)) == 0); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + { + INFO("storage offset alignment a power of two"); + CHECK(alignment > 0); + CHECK((alignment & (alignment - 1)) == 0); + } + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A compute program built through createShader links -- which GL refuses a compute object beside any other, so the // features' graphics objects stayed away -- from its compute object at #version 430, which it holds no longer // once linked; its work group size reads back; its block takes the table's point; and dispatchThreads over a count // that is not a multiple of the group size reaches each index exactly once. - template<> template<> - void llglslshader_object_t::test<11>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: createShader builds a compute program that reaches each index once", "[llglslshader_gl]") { setUpCompute(); - ensure_equals("a compute stage at GLSL 4.30", - compiledVersion(mShaders, "test/countC.glsl", GL_COMPUTE_SHADER, 0), std::string("#version 430")); - ensure("count program builds", - buildProgram(mCompute, "count (test)", { { "test/countC.glsl", GL_COMPUTE_SHADER } })); - ensure("it is compute", mCompute.isCompute()); - ensure("it holds no object once linked", attachedNames(mCompute.mProgramObject).empty()); - - ensure_equals("local size x", mCompute.mComputeLocalSize[0], 64u); - ensure_equals("local size y", mCompute.mComputeLocalSize[1], 1u); - ensure_equals("local size z", mCompute.mComputeLocalSize[2], 1u); - ensure("the table gives its block a point", mShaders.getStorageBlockBinding(TEST_COUNTS) >= 0); - ensure_equals("its block is at the table's point", mCompute.getStorageBinding(TEST_COUNTS), - mShaders.getStorageBlockBinding(TEST_COUNTS)); + CHECK(compiledVersion(mShaders, "test/countC.glsl", GL_COMPUTE_SHADER, 0) == std::string("#version 430")); + REQUIRE(buildProgram(mCompute, "count (test)", { { "test/countC.glsl", GL_COMPUTE_SHADER } })); + CHECK(mCompute.isCompute()); + UNSCOPED_INFO("it holds no object once linked"); + CHECK(attachedNames(mCompute.mProgramObject).empty()); + + CHECK(mCompute.mComputeLocalSize[0] == 64u); + CHECK(mCompute.mComputeLocalSize[1] == 1u); + CHECK(mCompute.mComputeLocalSize[2] == 1u); + UNSCOPED_INFO("the table gives its block a point"); + CHECK(mShaders.getStorageBlockBinding(TEST_COUNTS) >= 0); + UNSCOPED_INFO("its block is at the table's point"); + CHECK(mCompute.getStorageBinding(TEST_COUNTS) == mShaders.getStorageBlockBinding(TEST_COUNTS)); // 15.6 groups' worth, in a buffer with room for 17. constexpr U32 COUNT = 1000; constexpr U32 WORDS = 17 * 64; - ensure("counts allocate", mBuffers[0].allocate(WORDS * sizeof(U32), 0, nullptr, "counts (test)")); + REQUIRE(mBuffers[0].allocate(WORDS * sizeof(U32), 0, nullptr, "counts (test)")); mBuffers[0].clear(); mCompute.bind(); @@ -1126,47 +1125,43 @@ namespace tut const std::vector hits = readWords(mBuffers[0], WORDS); for (U32 i = 0; i < WORDS; ++i) { - ensure_equals("index " + std::to_string(i) + (i < COUNT ? " once" : " untouched"), hits[i], - i < COUNT ? 1u : 0u); + CAPTURE(i); + REQUIRE(hits[i] == (i < COUNT ? 1u : 0u)); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The #version a stage compiles at: the usual one for an ordinary program's own stage, never raised, and for the // shared objects likewise; 4.30 for a stage asking for it. A program whose stage reads a storage block builds only // when its features ask for 4.30, which createShader hands its own stages, and links with shared objects at the // usual version. On a context below 4.30 a compute program, or one asking for 4.30, does not build. - template<> template<> - void llglslshader_object_t::test<12>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: stages compile at the usual GLSL version unless they ask for 4.30", "[llglslshader_gl]") { setUpShaders(); if (gGLManager.mGLSLVersionMajor < 4) { - skip("the context's GLSL is below 4.00"); + SKIP("the context's GLSL is below 4.00"); } const std::string usual = gGLManager.mGLSLVersionMinor >= 20 ? "#version 420" : "#version 400"; - ensure("ordinary program builds", - buildProgram(mDraw, "ladder (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/solidF.glsl", GL_FRAGMENT_SHADER } })); - ensure_equals("an ordinary stage at the usual version", - compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 0), usual); + CHECK(buildProgram(mDraw, "ladder (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/solidF.glsl", GL_FRAGMENT_SHADER } })); + CHECK(compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 0) == usual); size_t shared = 0; for (const auto& stage : mShaders.mShaderObjects) { for (const auto& object : stage) { - ensure_equals(object.first + " at the usual version", firstSourceLine(object.second), usual); + CAPTURE(object.first); + REQUIRE(firstSourceLine(object.second) == usual); ++shared; } } - ensure_equals("the three shared objects", shared, size_t(3)); + CHECK(shared == size_t(3)); if (ll_test::contextHasCompute()) { - ensure_equals("a stage asking for 4.30 at 4.30", - compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 430), - std::string("#version 430")); + CHECK(compiledVersion(mShaders, "test/solidF.glsl", GL_FRAGMENT_SHADER, 430) == std::string("#version 430")); if (gGLManager.mMaxVertexShaderStorageBlocks >= 1) { const std::initializer_list> reader = @@ -1174,10 +1169,10 @@ namespace tut { "test/compactV.glsl", GL_VERTEX_SHADER }, { "test/compactF.glsl", GL_FRAGMENT_SHADER }, }; - ensure("a storage-block reader does not build at the usual version", - !buildProgram(mDraw2, "ladder reader (test)", reader)); - ensure("it builds asking for 4.30, beside the shared objects at the usual version", - buildProgram(mDraw2, "ladder reader 430 (test)", reader, 430)); + UNSCOPED_INFO("a storage-block reader does not build at the usual version"); + CHECK_FALSE(buildProgram(mDraw2, "ladder reader (test)", reader)); + UNSCOPED_INFO("it builds asking for 4.30, beside the shared objects at the usual version"); + CHECK(buildProgram(mDraw2, "ladder reader 430 (test)", reader, 430)); } } @@ -1194,39 +1189,41 @@ namespace tut 430); gGLManager.mGLSLVersionMajor = major; gGLManager.mGLSLVersionMinor = minor; - ensure("no compute program below 4.30", !compute_built); - ensure("no program asking for 4.30 below it", !raised_built); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("no compute program below 4.30"); + CHECK_FALSE(compute_built); + UNSCOPED_INFO("no program asking for 4.30 below it"); + CHECK_FALSE(raised_built); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A kernel stores into an R32F and an RGBA16F target through their programs' image units. Past the barriers the // textures hold the pattern, read back directly and sampled by a fragment program. - template<> template<> - void llglslshader_object_t::test<13>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a kernel stores into R32F and RGBA16F targets through image units", "[llglslshader_gl]") { setUpCompute(); constexpr U32 W = 37; constexpr U32 H = 23; - ensure("R32F target allocates", mTargets[0].allocate(W, H, GL_R32F)); - ensure("RGBA16F target allocates", mTargets[1].allocate(W, H, GL_RGBA16F)); - ensure("output target allocates", mTargets[2].allocate(W, H, GL_RGBA32F)); - ensure("store program builds", - buildProgram(mCompute, "image store (test)", { { "test/imageStoreC.glsl", GL_COMPUTE_SHADER } })); - ensure("sample program builds", - buildProgram(mDraw, "image sample (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(mTargets[0].allocate(W, H, GL_R32F)); + REQUIRE(mTargets[1].allocate(W, H, GL_RGBA16F)); + UNSCOPED_INFO("output target allocates"); + REQUIRE(mTargets[2].allocate(W, H, GL_RGBA32F)); + REQUIRE(buildProgram(mCompute, "image store (test)", { { "test/imageStoreC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mDraw, "image sample (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); const S32 r32f_unit = mCompute.getImageUnit(TEST_IMAGE_R32F); const S32 rgba_unit = mCompute.getImageUnit(TEST_IMAGE_RGBA16F); - ensure("R32F image has a unit", r32f_unit >= 0); - ensure("RGBA16F image has a unit", rgba_unit >= 0); - ensure("the units differ", r32f_unit != rgba_unit); + UNSCOPED_INFO("R32F image has a unit"); + CHECK(r32f_unit >= 0); + UNSCOPED_INFO("RGBA16F image has a unit"); + CHECK(rgba_unit >= 0); + CHECK(r32f_unit != rgba_unit); mCompute.bind(); - ensure_equals("R32F bound at its unit", - mCompute.bindImage(TEST_IMAGE_R32F, &mTargets[0], ALImageSlot::WRITE_ONLY), r32f_unit); - ensure_equals("RGBA16F bound at its unit", - mCompute.bindImage(TEST_IMAGE_RGBA16F, &mTargets[1], ALImageSlot::WRITE_ONLY), rgba_unit); + UNSCOPED_INFO("R32F bound at its unit"); + CHECK(mCompute.bindImage(TEST_IMAGE_R32F, &mTargets[0], ALImageSlot::WRITE_ONLY) == r32f_unit); + UNSCOPED_INFO("RGBA16F bound at its unit"); + CHECK(mCompute.bindImage(TEST_IMAGE_RGBA16F, &mTargets[1], ALImageSlot::WRITE_ONLY) == rgba_unit); mCompute.dispatchThreads(W, H); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_FETCH | LLRender::BARRIER_TEXTURE_UPDATE); mCompute.unbindImage(TEST_IMAGE_R32F); @@ -1238,24 +1235,25 @@ namespace tut { for (U32 x = 0; x < W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = (size_t)y * W + x; - ensure_equals("R32F" + at, r32f[i], F32(x * 1000 + y)); - ensure_equals("RGBA16F r" + at, rgba[i * 4 + 0], F32(x) * 0.125f); - ensure_equals("RGBA16F g" + at, rgba[i * 4 + 1], F32(y) * 0.125f); - ensure_equals("RGBA16F b" + at, rgba[i * 4 + 2], 0.25f); - ensure_equals("RGBA16F a" + at, rgba[i * 4 + 3], 1.f); + REQUIRE(r32f[i] == F32(x * 1000 + y)); + REQUIRE(rgba[i * 4 + 0] == F32(x) * 0.125f); + REQUIRE(rgba[i * 4 + 1] == F32(y) * 0.125f); + REQUIRE(rgba[i * 4 + 2] == 0.25f); + REQUIRE(rgba[i * 4 + 3] == 1.f); } } LLGLDisable blend(GL_BLEND); for (U32 source = 0; source < 2; ++source) { + INFO("sampling the " << (source ? "RGBA16F" : "R32F") << " target"); mTargets[2].bindTarget(); gGL.setClearColor(LLColor4::transparent); mTargets[2].clear(); mDraw.bind(); - ensure("source bound", mDraw.bindTexture(TEST_SOURCE_MAP, &mTargets[source], ALSamplers::PointClamp) >= 0); + REQUIRE(mDraw.bindTexture(TEST_SOURCE_MAP, &mTargets[source], ALSamplers::PointClamp) >= 0); gGL.drawArraysInstanced(LLRender::TRIANGLES, 0, 3, 1); const std::vector drawn = readBoundTarget(W, H, 4, GL_RGBA); mTargets[2].flush(); @@ -1265,8 +1263,7 @@ namespace tut { for (U32 x = 0; x < W; ++x) { - const std::string at = (source ? " RGBA16F" : " R32F") + std::string(" sampled at ") - + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = ((size_t)y * W + x) * 4; const F32 expected[4] = { @@ -1277,25 +1274,25 @@ namespace tut }; for (U32 c = 0; c < 4; ++c) { - ensure_equals("component " + std::to_string(c) + at, drawn[i + c], expected[c]); + CAPTURE(c); + REQUIRE(drawn[i + c] == expected[c]); } } } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // GPU compaction drawn indirectly: a kernel keeps the ids passing a test, counting them into the instance count // of the draw command at the head of their list; past the barriers an indirect draw, whose vertex stage reads the // same buffer as a storage block, puts one point at each kept id's pixel and none anywhere else. The list is bound // once, for the kernel, and the draw finds it at the same point. - template<> template<> - void llglslshader_object_t::test<14>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a compacted id list drawn indirectly puts one point at each kept id", "[llglslshader_gl]") { setUpCompute(); if (gGLManager.mMaxVertexShaderStorageBlocks < 1) { - skip("the context's vertex stage reads no storage blocks"); + SKIP("the context's vertex stage reads no storage blocks"); } constexpr U32 COUNT = 100; constexpr U32 SIDE = 16; @@ -1304,17 +1301,18 @@ namespace tut // One vertex a point, no instances yet, then room for every id. std::vector list(4 + COUNT, 0u); list[0] = 1u; - ensure("list allocates", mBuffers[0].allocate(list.size() * sizeof(U32), 0, list.data(), "compaction (test)")); - ensure("output target allocates", mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - ensure("compaction builds", - buildProgram(mCompute, "compaction (test)", { { "test/compactC.glsl", GL_COMPUTE_SHADER } })); - ensure("draw builds", - buildProgram(mDraw, "compaction draw (test)", - { { "test/compactV.glsl", GL_VERTEX_SHADER }, { "test/compactF.glsl", GL_FRAGMENT_SHADER } }, - 430)); + REQUIRE(mBuffers[0].allocate(list.size() * sizeof(U32), 0, list.data(), "compaction (test)")); + UNSCOPED_INFO("output target allocates"); + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(buildProgram(mCompute, "compaction (test)", { { "test/compactC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mDraw, "compaction draw (test)", + { { "test/compactV.glsl", GL_VERTEX_SHADER }, { "test/compactF.glsl", GL_FRAGMENT_SHADER } }, + 430)); const S32 point = mShaders.getStorageBlockBinding(TEST_DRAW_LIST); - ensure_equals("the kernel's list at the table's point", mCompute.getStorageBinding(TEST_DRAW_LIST), point); - ensure_equals("and the draw's", mDraw.getStorageBinding(TEST_DRAW_LIST), point); + UNSCOPED_INFO("the kernel's list at the table's point"); + CHECK(mCompute.getStorageBinding(TEST_DRAW_LIST) == point); + UNSCOPED_INFO("and the draw's"); + CHECK(mDraw.getStorageBinding(TEST_DRAW_LIST) == point); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, (GLint)COUNT); @@ -1328,7 +1326,8 @@ namespace tut mTargets[0].clear(); mDraw.bind(); mDraw.uniform1i(TEST_WIDTH, (GLint)SIDE); - ensure_equals("the list still bound for the draw", gGL.getStorageBuffer((U32)point), mBuffers[0].getGLName()); + UNSCOPED_INFO("the list still bound for the draw"); + CHECK(gGL.getStorageBuffer((U32)point) == mBuffers[0].getGLName()); gGL.drawArraysIndirect(LLRender::POINTS, mBuffers[0], 0); const std::vector drawn = readBoundTarget(SIDE, SIDE, 1, GL_RED); mTargets[0].flush(); @@ -1336,26 +1335,24 @@ namespace tut for (U32 id = 0; id < SIDE * SIDE; ++id) { const bool kept = id < COUNT && id % 3 == 0; - ensure_equals("id " + std::to_string(id) + (kept ? " drawn" : " not drawn"), drawn[id], - kept ? F32(id + 1) : 0.f); + CAPTURE(id, kept); + REQUIRE(drawn[id] == (kept ? F32(id + 1) : 0.f)); } - ensure_equals("instances counted", readWords(mBuffers[0], 2)[1], KEPT); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("instances counted"); + CHECK(readWords(mBuffers[0], 2)[1] == KEPT); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // dispatchIndirect runs the group counts a buffer holds, here written by a kernel before it 16 bytes in: every // invocation of 5 x 2 x 1 groups of 4 counted once, and the last group seen in each direction. - template<> template<> - void llglslshader_object_t::test<15>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: dispatchIndirect runs the group counts a buffer holds", "[llglslshader_gl]") { setUpCompute(); const std::vector zeros(8, 0u); - ensure("command buffer allocates", mBuffers[0].allocate(8 * sizeof(U32), 0, zeros.data(), "dispatch (test)")); - ensure("counts allocate", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); - ensure("writer builds", - buildProgram(mCompute, "dispatch writer (test)", { { "test/indirectWriteC.glsl", GL_COMPUTE_SHADER } })); - ensure("counter builds", - buildProgram(mCompute2, "dispatch counter (test)", { { "test/indirectCountC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(mBuffers[0].allocate(8 * sizeof(U32), 0, zeros.data(), "dispatch (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); + REQUIRE(buildProgram(mCompute, "dispatch writer (test)", { { "test/indirectWriteC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute2, "dispatch counter (test)", { { "test/indirectCountC.glsl", GL_COMPUTE_SHADER } })); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, 5); @@ -1368,40 +1365,52 @@ namespace tut mCompute2.dispatchIndirect(mBuffers[0], 16); const std::vector command = readWords(mBuffers[0], 7); - ensure_equals("command x", command[4], 5u); - ensure_equals("command y", command[5], 2u); - ensure_equals("command z", command[6], 1u); + UNSCOPED_INFO("command x"); + CHECK(command[4] == 5u); + UNSCOPED_INFO("command y"); + CHECK(command[5] == 2u); + UNSCOPED_INFO("command z"); + CHECK(command[6] == 1u); const std::vector hits = readWords(mBuffers[1], 4); - ensure_equals("invocations", hits[0], 5u * 2u * 1u * 4u); - ensure_equals("groups in x", hits[1], 5u); - ensure_equals("groups in y", hits[2], 2u); - ensure_equals("groups in z", hits[3], 1u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("invocations"); + CHECK(hits[0] == 5u * 2u * 1u * 4u); + UNSCOPED_INFO("groups in x"); + CHECK(hits[1] == 5u); + UNSCOPED_INFO("groups in y"); + CHECK(hits[2] == 2u); + UNSCOPED_INFO("groups in z"); + CHECK(hits[3] == 1u); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Two storage blocks declared in the order opposite to the table's take the table's points, which run in its // order, and bindStorageBuffer routes each buffer to its own block: swapping the buffers swaps what each holds. - template<> template<> - void llglslshader_object_t::test<16>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: storage blocks declared out of the table's order still take its points", "[llglslshader_gl]") { setUpCompute(); - ensure("remap builds", buildProgram(mCompute, "block remap (test)", { { "test/remapC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "block remap (test)", { { "test/remapC.glsl", GL_COMPUTE_SHADER } })); const S32 first = mShaders.getStorageBlockBinding(TEST_BLOCK_FIRST); - ensure_equals("the first block at its point", mCompute.getStorageBinding(TEST_BLOCK_FIRST), first); - ensure_equals("the second at the next", mCompute.getStorageBinding(TEST_BLOCK_SECOND), first + 1); - ensure_equals("an undeclared block has none", mCompute.getStorageBinding(TEST_COUNTS), -1); - ensure_equals("nor does an index past the table", mCompute.getStorageBinding(TEST_STORAGE_END), -1); + UNSCOPED_INFO("the first block at its point"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_FIRST) == first); + UNSCOPED_INFO("the second at the next"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_SECOND) == first + 1); + UNSCOPED_INFO("an undeclared block has none"); + CHECK(mCompute.getStorageBinding(TEST_COUNTS) == -1); + UNSCOPED_INFO("nor does an index past the table"); + CHECK(mCompute.getStorageBinding(TEST_STORAGE_END) == -1); const std::vector zeros(4, 0u); - ensure("a allocates", mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "a (test)")); - ensure("b allocates", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "b (test)")); + REQUIRE(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "a (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "b (test)")); mCompute.bind(); mCompute.bindStorageBuffer(TEST_BLOCK_FIRST, mBuffers[0]); mCompute.bindStorageBuffer(TEST_BLOCK_SECOND, mBuffers[1]); mCompute.dispatch(1); - ensure_equals("a holds the first block", readWords(mBuffers[0], 1)[0], 111u); - ensure_equals("b holds the second block", readWords(mBuffers[1], 1)[0], 222u); + UNSCOPED_INFO("a holds the first block"); + CHECK(readWords(mBuffers[0], 1)[0] == 111u); + UNSCOPED_INFO("b holds the second block"); + CHECK(readWords(mBuffers[1], 1)[0] == 222u); mBuffers[0].clear(); mBuffers[1].clear(); @@ -1409,31 +1418,36 @@ namespace tut mCompute.bindStorageBuffer(TEST_BLOCK_FIRST, mBuffers[1]); mCompute.bindStorageBuffer(TEST_BLOCK_SECOND, mBuffers[0]); mCompute.dispatch(1); - ensure_equals("swapped, a holds the second block", readWords(mBuffers[0], 1)[0], 222u); - ensure_equals("swapped, b holds the first block", readWords(mBuffers[1], 1)[0], 111u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("swapped, a holds the second block"); + CHECK(readWords(mBuffers[0], 1)[0] == 222u); + UNSCOPED_INFO("swapped, b holds the first block"); + CHECK(readWords(mBuffers[1], 1)[0] == 111u); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Two image uniforms, declared the opposite way round, take distinct units in reserved order, and bindImage // routes each texture to its own. - template<> template<> - void llglslshader_object_t::test<17>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: image uniforms declared out of order take units in reserved order", "[llglslshader_gl]") { setUpCompute(); constexpr U32 SIDE = 16; - ensure("units builds", - buildProgram(mCompute, "image units (test)", { { "test/imageUnitsC.glsl", GL_COMPUTE_SHADER } })); - ensure_equals("the first reserved image first", mCompute.getImageUnit(TEST_IMAGE_A), 0); - ensure_equals("the second reserved image second", mCompute.getImageUnit(TEST_IMAGE_B), 1); - ensure_equals("an undeclared image has none", mCompute.getImageUnit(TEST_IMAGE_R32F), -1); - ensure_equals("a sampler has none", mCompute.getImageUnit(LLShaderMgr::DIFFUSE_MAP), -1); - ensure_equals("nor a texture unit", mCompute.getTextureChannel(TEST_IMAGE_A), -1); - - ensure("a allocates", mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - ensure("b allocates", mTargets[1].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(buildProgram(mCompute, "image units (test)", { { "test/imageUnitsC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("the first reserved image first"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_A) == 0); + UNSCOPED_INFO("the second reserved image second"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_B) == 1); + UNSCOPED_INFO("an undeclared image has none"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_R32F) == -1); + UNSCOPED_INFO("a sampler has none"); + CHECK(mCompute.getImageUnit(LLShaderMgr::DIFFUSE_MAP) == -1); + UNSCOPED_INFO("nor a texture unit"); + CHECK(mCompute.getTextureChannel(TEST_IMAGE_A) == -1); + + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(mTargets[1].allocate(SIDE, SIDE, GL_R32F)); mCompute.bind(); - ensure_equals("a bound", mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY), 0); - ensure_equals("b bound", mCompute.bindImage(TEST_IMAGE_B, &mTargets[1], ALImageSlot::WRITE_ONLY), 1); + CHECK(mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY) == 0); + CHECK(mCompute.bindImage(TEST_IMAGE_B, &mTargets[1], ALImageSlot::WRITE_ONLY) == 1); mCompute.dispatchThreads(SIDE, SIDE); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); mCompute.unbindImage(TEST_IMAGE_A); @@ -1443,26 +1457,27 @@ namespace tut const std::vector b = readTexture(mTargets[1].getTexture(), SIDE, SIDE, 1, GL_RED, GL_FLOAT); for (U32 i = 0; i < SIDE * SIDE; ++i) { - ensure_equals("a holds image A at " + std::to_string(i), a[i], 1.f); - ensure_equals("b holds image B at " + std::to_string(i), b[i], 2.f); + CAPTURE(i); + UNSCOPED_INFO("a holds image A"); + REQUIRE(a[i] == 1.f); + UNSCOPED_INFO("b holds image B"); + REQUIRE(b[i] == 2.f); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A usampler2D under a reserved name gets a texture unit, the one after a float sampler's, and reads an R32UI // texture's values exactly, past what eight bits could hold. - template<> template<> - void llglslshader_object_t::test<18>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a usampler2D takes the unit after a float one and reads R32UI exactly", "[llglslshader_gl]") { setUpCompute(); constexpr U32 W = 13; constexpr U32 H = 7; - ensure("sample builds", - buildProgram(mCompute, "uint sample (test)", { { "test/uintSampleC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "uint sample (test)", { { "test/uintSampleC.glsl", GL_COMPUTE_SHADER } })); const S32 float_unit = mCompute.getTextureChannel(LLShaderMgr::DIFFUSE_MAP); const S32 uint_unit = mCompute.getTextureChannel(TEST_UINT_MAP); - ensure_equals("the float sampler first", float_unit, 0); - ensure_equals("the unsigned one after it", uint_unit, 1); + CHECK(float_unit == 0); + CHECK(uint_unit == 1); std::vector values(W * H); for (U32 i = 0; i < W * H; ++i) @@ -1471,17 +1486,15 @@ namespace tut } mUintImage = new LLImageGL(W, H, 1, /*usemipmaps=*/false); mUintImage->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - ensure("R32UI image created", mUintImage->createGLTexture(0, reinterpret_cast(values.data()))); - ensure("RGBA8 target allocates", mTargets[1].allocate(W, H, GL_RGBA8)); - ensure("out allocates", mBuffers[0].allocate(W * H * sizeof(U32), 0, nullptr, "uint sample (test)")); + REQUIRE(mUintImage->createGLTexture(0, reinterpret_cast(values.data()))); + REQUIRE(mTargets[1].allocate(W, H, GL_RGBA8)); + REQUIRE(mBuffers[0].allocate(W * H * sizeof(U32), 0, nullptr, "uint sample (test)")); mBuffers[0].clear(0xFFFFFFFFu); mCompute.bind(); mCompute.uniform1i(TEST_COUNT, 1); - ensure_equals("float texture bound", mCompute.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mTargets[1], ALSamplers::PointClamp), - float_unit); - ensure("unsigned texture bound", - gGL.getTextureSlot((U32)uint_unit)->bindSampled(mUintImage.get(), ALSamplers::PointClamp)); + CHECK(mCompute.bindTexture(LLShaderMgr::DIFFUSE_MAP, &mTargets[1], ALSamplers::PointClamp) == float_unit); + CHECK(gGL.getTextureSlot((U32)uint_unit)->bindSampled(mUintImage.get(), ALSamplers::PointClamp)); mCompute.bindStorageBuffer(TEST_COUNTS, mBuffers[0]); mCompute.dispatchThreads(W, H); mCompute.unbindTexture(LLShaderMgr::DIFFUSE_MAP); @@ -1490,36 +1503,37 @@ namespace tut const std::vector read = readWords(mBuffers[0], W * H); for (U32 i = 0; i < W * H; ++i) { - ensure_equals("texel " + std::to_string(i), read[i], values[i]); + CAPTURE(i); + REQUIRE(read[i] == values[i]); } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Integer targets read through bindTexture with no sampler named: an R32UI and an RG16I target, each written by a // program's integer output, come back texel for texel through a usampler2D and an isampler2D. A filtering sampler // leaves an integer texture incomplete, reading 0, so the default for an integer attachment is point, attachment 0 // included. - template<> template<> - void llglslshader_object_t::test<19>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: bindTexture with no sampler named point-samples integer targets", "[llglslshader_gl]") { setUpShaders(); constexpr U32 W = 11; constexpr U32 H = 6; - ensure("R32UI target allocates", mTargets[0].allocate(W, H, GL_R32UI)); - ensure("RG16I target allocates", mTargets[1].allocate(W, H, GL_RG16I)); - ensure("output target allocates", mTargets[2].allocate(W, H, GL_RGBA32UI)); - ensure("its signed attachment", mTargets[2].addColorAttachment(GL_RGBA32I)); - ensure("unsigned writer builds", - buildProgram(mDraw, "uint write (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/uintWriteF.glsl", GL_FRAGMENT_SHADER } })); - ensure("signed writer builds", - buildProgram(mDraw2, "int write (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intWriteF.glsl", GL_FRAGMENT_SHADER } })); - ensure("reader builds", - buildProgram(mDraw3, "integer sample (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); - ensure("the unsigned sampler has a unit", mDraw3.getTextureChannel(TEST_UINT_MAP) >= 0); - ensure("the signed sampler has a unit", mDraw3.getTextureChannel(TEST_INT_MAP) >= 0); + REQUIRE(mTargets[0].allocate(W, H, GL_R32UI)); + REQUIRE(mTargets[1].allocate(W, H, GL_RG16I)); + UNSCOPED_INFO("output target allocates"); + REQUIRE(mTargets[2].allocate(W, H, GL_RGBA32UI)); + UNSCOPED_INFO("the output's signed attachment"); + REQUIRE(mTargets[2].addColorAttachment(GL_RGBA32I)); + REQUIRE(buildProgram(mDraw, "uint write (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/uintWriteF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw2, "int write (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intWriteF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw3, "integer sample (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); + UNSCOPED_INFO("the unsigned sampler has a unit"); + CHECK(mDraw3.getTextureChannel(TEST_UINT_MAP) >= 0); + UNSCOPED_INFO("the signed sampler has a unit"); + CHECK(mDraw3.getTextureChannel(TEST_INT_MAP) >= 0); LLGLDisable blend(GL_BLEND); for (U32 source = 0; source < 2; ++source) @@ -1532,12 +1546,13 @@ namespace tut mTargets[2].bindTarget(); mDraw3.bind(); - ensure("unsigned source bound", mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0]) >= 0); - ensure("signed source bound", mDraw3.bindTexture(TEST_INT_MAP, &mTargets[1]) >= 0); + CHECK(mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0]) >= 0); + CHECK(mDraw3.bindTexture(TEST_INT_MAP, &mTargets[1]) >= 0); // Read off GL, since a driver may read an integer texture under a filtering sampler texel by texel anyway. for (const S32 uniform : { (S32)TEST_UINT_MAP, (S32)TEST_INT_MAP }) { + INFO((uniform == TEST_UINT_MAP ? "the unsigned source's sampler" : "the signed source's sampler")); glActiveTexture(GL_TEXTURE0 + mDraw3.getTextureChannel(uniform)); GLint sampler = 0; glGetIntegerv(GL_SAMPLER_BINDING, &sampler); @@ -1545,10 +1560,8 @@ namespace tut GLint mag_filter = 0; glGetSamplerParameteriv((GLuint)sampler, GL_TEXTURE_MIN_FILTER, &min_filter); glGetSamplerParameteriv((GLuint)sampler, GL_TEXTURE_MAG_FILTER, &mag_filter); - const std::string which = uniform == TEST_UINT_MAP ? "the unsigned source's" : "the signed source's"; - ensure_equals(which + " sampler magnifies by the nearest texel", (U32)mag_filter, (U32)GL_NEAREST); - ensure_equals(which + " sampler minifies by the nearest texel", (U32)min_filter, - (U32)GL_NEAREST_MIPMAP_NEAREST); + REQUIRE((U32)mag_filter == (U32)GL_NEAREST); + REQUIRE((U32)min_filter == (U32)GL_NEAREST_MIPMAP_NEAREST); } glActiveTexture(GL_TEXTURE0 + gGL.getCurrentTexUnitIndex()); // And the debug validator, which checks every sampler a draw reads, finds nothing to say -- where a filtering @@ -1558,8 +1571,10 @@ namespace tut mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0], ALSamplers::BilinearClamp); const bool filtered_valid = validate_bound_samplers(); gDebugGL = false; - ensure("the debug validator passes the binds", samplers_valid); - ensure("and refuses a filtering sampler over an integer texture", !filtered_valid); + UNSCOPED_INFO("the debug validator passes the binds"); + CHECK(samplers_valid); + UNSCOPED_INFO("and refuses a filtering sampler over an integer texture"); + CHECK_FALSE(filtered_valid); mDraw3.bindTexture(TEST_UINT_MAP, &mTargets[0]); gGL.drawArraysInstanced(LLRender::TRIANGLES, 0, 3, 1); @@ -1577,18 +1592,19 @@ namespace tut { for (U32 x = 0; x < W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = ((size_t)y * W + x) * 4; const U32 uint_expected[4] = { 0x01000001u + y * 1000u + x, 0u, 0u, 1u }; const S32 int_expected[4] = { -1 - (S32)x, (S32)y * 300 - 1000, 0, 1 }; for (U32 c = 0; c < 4; ++c) { - ensure_equals("unsigned component " + std::to_string(c) + at, uints[i + c], uint_expected[c]); - ensure_equals("signed component " + std::to_string(c) + at, ints[i + c], int_expected[c]); + CAPTURE(c); + REQUIRE(uints[i + c] == uint_expected[c]); + REQUIRE(ints[i + c] == int_expected[c]); } } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A shared object is the shader manager's, not the programs' that attach it. Two programs built through @@ -1598,16 +1614,17 @@ namespace tut // built after both are gone attaches the shared object again and links. One that fails to link deletes its own // objects all the same. clearShaderObjects deletes every shared object at once, as no linked program holds one, // and the programs draw on without them. - template<> template<> - void llglslshader_object_t::test<20>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a shared object belongs to the shader manager and not to its programs", "[llglslshader_gl]") { setUpShaders(); constexpr U32 SIDE = 4; - ensure("output target allocates", mTargets[0].allocate(SIDE, SIDE, GL_RGBA32F)); + UNSCOPED_INFO("output target allocates"); + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_RGBA32F)); const auto& fragments = mShaders.mShaderObjects[LLShaderMgr::STAGE_FRAGMENT]; const auto found = fragments.find("deferred/globalF.glsl"); - ensure("the shared object compiled", found != fragments.end()); + UNSCOPED_INFO("the shared object compiled"); + REQUIRE(found != fragments.end()); const GLuint shared = found->second; const std::initializer_list> files = @@ -1617,10 +1634,11 @@ namespace tut }; const U32 live = LLShaderMgr::sLiveObjects; - // What `program` draws, every pixel the colour the stub passes through. + // That `program` draws every pixel the colour the stub passes through. LLGLDisable blend(GL_BLEND); - auto ensureDraws = [&](const std::string& what, LLGLSLShader& program) + auto require_draws = [&](const std::string& what, LLGLSLShader& program) { + INFO(what); mTargets[0].bindTarget(); gGL.setClearColor(LLColor4::transparent); mTargets[0].clear(); @@ -1632,41 +1650,55 @@ namespace tut const F32 expected[4] = { 0.25f, 0.5f, 0.75f, 1.f }; for (size_t i = 0; i < drawn.size(); ++i) { - ensure_equals(what + ", component " + std::to_string(i), drawn[i], expected[i % 4]); + CAPTURE(i); + REQUIRE(drawn[i] == expected[i % 4]); } }; - ensure("A builds", buildProgram(mDraw, "shared A (test)", files)); - ensure("B builds", buildProgram(mDraw2, "shared B (test)", files)); - ensure("A holds no object once linked", attachedNames(mDraw.mProgramObject).empty()); - ensure("nor does B", attachedNames(mDraw2.mProgramObject).empty()); - ensure_equals("their own objects deleted", LLShaderMgr::sLiveObjects, live); - ensure("a program's own stage is not shared", !fragments.contains("test/globalCallF.glsl")); - ensure("the shared object stays", glIsShader(shared) == GL_TRUE); - ensure("not even flagged for deletion", !flaggedForDeletion(shared)); - ensureDraws("A draws", mDraw); + REQUIRE(buildProgram(mDraw, "shared A (test)", files)); + REQUIRE(buildProgram(mDraw2, "shared B (test)", files)); + UNSCOPED_INFO("A holds no object once linked"); + CHECK(attachedNames(mDraw.mProgramObject).empty()); + UNSCOPED_INFO("nor does B"); + CHECK(attachedNames(mDraw2.mProgramObject).empty()); + UNSCOPED_INFO("their own objects deleted"); + CHECK(LLShaderMgr::sLiveObjects == live); + UNSCOPED_INFO("a program's own stage is not shared"); + CHECK_FALSE(fragments.contains("test/globalCallF.glsl")); + UNSCOPED_INFO("the shared object stays"); + CHECK(glIsShader(shared) == GL_TRUE); + UNSCOPED_INFO("not even flagged for deletion"); + CHECK_FALSE(flaggedForDeletion(shared)); + require_draws("A draws", mDraw); mDraw.unload(); - ensure("unloading A leaves the shared object", glIsShader(shared) == GL_TRUE && !flaggedForDeletion(shared)); + UNSCOPED_INFO("unloading A leaves the shared object"); + CHECK((glIsShader(shared) == GL_TRUE && !flaggedForDeletion(shared))); glValidateProgram(mDraw2.mProgramObject); GLint valid = GL_FALSE; glGetProgramiv(mDraw2.mProgramObject, GL_VALIDATE_STATUS, &valid); - ensure("B validates", valid == GL_TRUE); - ensureDraws("B draws", mDraw2); + CHECK(valid == GL_TRUE); + require_draws("B draws", mDraw2); mDraw2.unload(); - ensure("C builds afterwards", buildProgram(mDraw3, "shared C (test)", files)); - ensure("C holds no object once linked", attachedNames(mDraw3.mProgramObject).empty()); - ensureDraws("C draws", mDraw3); + UNSCOPED_INFO("C builds after A and B are gone"); + REQUIRE(buildProgram(mDraw3, "shared C (test)", files)); + UNSCOPED_INFO("C holds no object once linked"); + CHECK(attachedNames(mDraw3.mProgramObject).empty()); + require_draws("C draws", mDraw3); // Without the shared object to call into, the stage cannot link, at any class. - ensure("D does not link", !buildProgram(mDraw, "unlinked D (test)", files, 0, /*attach_nothing=*/true)); - ensure_equals("and its own objects are deleted all the same", LLShaderMgr::sLiveObjects, live); + UNSCOPED_INFO("D does not link"); + CHECK_FALSE(buildProgram(mDraw, "unlinked D (test)", files, 0, /*attach_nothing=*/true)); + UNSCOPED_INFO("and its own objects are deleted all the same"); + CHECK(LLShaderMgr::sLiveObjects == live); S32 level = 1; const GLuint loose = mShaders.loadShaderFile("test/solidF.glsl", level, GL_FRAGMENT_SHADER); - ensure("an object no program holds", loose != 0 && glIsShader(loose) == GL_TRUE); - ensure_equals("counted live", LLShaderMgr::sLiveObjects, live + 1); + UNSCOPED_INFO("an object no program holds"); + CHECK((loose != 0 && glIsShader(loose) == GL_TRUE)); + UNSCOPED_INFO("the loose object counted live"); + CHECK(LLShaderMgr::sLiveObjects == live + 1); size_t held = 0; for (const auto& stage : mShaders.mShaderObjects) @@ -1676,13 +1708,17 @@ namespace tut mShaders.clearShaderObjects(); for (const auto& stage : mShaders.mShaderObjects) { - ensure("the manager holds no object", stage.empty()); + UNSCOPED_INFO("the manager holds no object"); + REQUIRE(stage.empty()); } - ensure("one no program ever held goes at once", glIsShader(loose) == GL_FALSE); - ensure("and one programs linked with", glIsShader(shared) == GL_FALSE); - ensure_equals("none counted live", LLShaderMgr::sLiveObjects, live + 1 - (U32)held); - ensureDraws("C still draws", mDraw3); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("one no program ever held goes at once"); + CHECK(glIsShader(loose) == GL_FALSE); + UNSCOPED_INFO("and one programs linked with"); + CHECK(glIsShader(shared) == GL_FALSE); + UNSCOPED_INFO("none counted live"); + CHECK(LLShaderMgr::sLiveObjects == live + 1 - (U32)held); + require_draws("C still draws", mDraw3); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A buffer bound once at a reserved block's point serves every program after it: one program declaring the block @@ -1690,23 +1726,23 @@ namespace tut // between -- where a numbering by program would put the shared block at point 0 for the first, the second block's // place. And LLRender keeps the points: a bind of what a point holds issues nothing, which a bind made behind its // back shows, and refreshState puts back what it holds. - template<> template<> - void llglslshader_object_t::test<21>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a buffer bound once at a block's point serves every program after it", "[llglslshader_gl]") { setUpCompute(); - ensure("first-only builds", - buildProgram(mCompute, "first only (test)", { { "test/firstOnlyC.glsl", GL_COMPUTE_SHADER } })); - ensure("first-and-counts builds", - buildProgram(mCompute2, "first and counts (test)", { { "test/firstAndCountsC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "first only (test)", { { "test/firstOnlyC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute2, "first and counts (test)", { { "test/firstAndCountsC.glsl", GL_COMPUTE_SHADER } })); const S32 first = mShaders.getStorageBlockBinding(TEST_BLOCK_FIRST); const S32 counts = mShaders.getStorageBlockBinding(TEST_COUNTS); - ensure_equals("the first program's block at the table's point", mCompute.getStorageBinding(TEST_BLOCK_FIRST), first); - ensure_equals("the second's too", mCompute2.getStorageBinding(TEST_BLOCK_FIRST), first); - ensure("the two blocks apart", first != counts); + UNSCOPED_INFO("the first program's block at the table's point"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_FIRST) == first); + UNSCOPED_INFO("the second's too"); + CHECK(mCompute2.getStorageBinding(TEST_BLOCK_FIRST) == first); + UNSCOPED_INFO("the two blocks apart"); + CHECK(first != counts); const std::vector zeros(4, 0u); - ensure("first allocates", mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "first (test)")); - ensure("counts allocate", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); + REQUIRE(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "first (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "counts (test)")); mCompute.bind(); gGL.bindStorageBuffer((U32)first, mBuffers[0]); @@ -1716,9 +1752,12 @@ namespace tut mCompute2.dispatch(1); const std::vector held_first = readWords(mBuffers[0], 2); - ensure_equals("the first program wrote the first block", held_first[0], 7u); - ensure_equals("the second program wrote it too", held_first[1], 9u); - ensure_equals("and its counts", readWords(mBuffers[1], 1)[0], 5u); + UNSCOPED_INFO("the first program wrote the first block"); + CHECK(held_first[0] == 7u); + UNSCOPED_INFO("the second program wrote it too"); + CHECK(held_first[1] == 9u); + UNSCOPED_INFO("and its counts"); + CHECK(readWords(mBuffers[1], 1)[0] == 5u); // Behind LLRender's back, then the bind it holds already: nothing issued, so GL keeps the other buffer. auto bound_at = [](S32 point) @@ -1729,90 +1768,102 @@ namespace tut }; glBindBufferBase(GL_SHADER_STORAGE_BUFFER, (GLuint)first, mBuffers[1].getGLName()); gGL.bindStorageBuffer((U32)first, mBuffers[0]); - ensure_equals("a bind of what the point holds issues nothing", bound_at(first), mBuffers[1].getGLName()); + UNSCOPED_INFO("a bind of what the point holds issues nothing"); + CHECK(bound_at(first) == mBuffers[1].getGLName()); gGL.refreshState(); - ensure_equals("refreshState puts back what it holds", bound_at(first), mBuffers[0].getGLName()); + UNSCOPED_INFO("refreshState puts back what it holds"); + CHECK(bound_at(first) == mBuffers[0].getGLName()); // A range, at a multiple of the context's alignment at least 16 bytes in, is a different bind from the whole // buffer. const size_t alignment = (size_t)gGLManager.mShaderStorageBufferOffsetAlignment; const size_t offset = (16 + alignment - 1) / alignment * alignment; ALGPUBuffer wide; - ensure("wide allocates", wide.allocate(offset + 4 * sizeof(U32), 0, nullptr, "wide (test)")); + REQUIRE(wide.allocate(offset + 4 * sizeof(U32), 0, nullptr, "wide (test)")); wide.clear(); mCompute.bind(); gGL.bindStorageBuffer((U32)first, wide, offset, 4 * sizeof(U32)); mCompute.dispatch(1); std::vector words((offset + 4 * sizeof(U32)) / sizeof(U32), 0xFFFFFFFFu); wide.read(0, words.data(), words.size() * sizeof(U32)); - ensure_equals("the range's first word written", words[offset / sizeof(U32)], 7u); - ensure_equals("the word before it not", words[offset / sizeof(U32) - 1], 0u); + UNSCOPED_INFO("the range's first word written"); + CHECK(words[offset / sizeof(U32)] == 7u); + UNSCOPED_INFO("the word before it not"); + CHECK(words[offset / sizeof(U32) - 1] == 0u); // Releasing a bound buffer forgets it. wide.release(); - ensure_equals("a released buffer leaves its point empty", gGL.getStorageBuffer((U32)first), 0u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("a released buffer leaves its point empty"); + CHECK(gGL.getStorageBuffer((U32)first) == 0u); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An array of blocks takes one point per element from the table's start for it, and bindStorageBuffer names the // element: each of two buffers takes its element's writes. A program reading only the second element finds it // at its point all the same, though GL may report no first element for it. - template<> template<> - void llglslshader_object_t::test<22>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: a block array takes consecutive points and binds buffers by element", "[llglslshader_gl]") { setUpCompute(); - ensure("array program builds", - buildProgram(mCompute, "block array (test)", { { "test/blockArrayC.glsl", GL_COMPUTE_SHADER } })); - ensure("second-element program builds", - buildProgram(mCompute2, "block array one (test)", { { "test/blockArrayOneC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute, "block array (test)", { { "test/blockArrayC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(buildProgram(mCompute2, "block array one (test)", { { "test/blockArrayOneC.glsl", GL_COMPUTE_SHADER } })); const S32 base = mShaders.getStorageBlockBinding(TEST_BLOCK_ARRAY); - ensure("the table gives the array a point", base >= 0); - ensure_equals("element 0 at it", mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 0), base); - ensure_equals("element 1 at the next", mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 1), base + 1); - ensure_equals("no element 2", mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 2), -1); - ensure_equals("the second-element program's element 1 there too", mCompute2.getStorageBinding(TEST_BLOCK_ARRAY, 1), - base + 1); + UNSCOPED_INFO("the table gives the array a point"); + CHECK(base >= 0); + UNSCOPED_INFO("element 0 at it"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 0) == base); + UNSCOPED_INFO("element 1 at the next"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 1) == base + 1); + UNSCOPED_INFO("no element 2"); + CHECK(mCompute.getStorageBinding(TEST_BLOCK_ARRAY, 2) == -1); + UNSCOPED_INFO("the second-element program's element 1 there too"); + CHECK(mCompute2.getStorageBinding(TEST_BLOCK_ARRAY, 1) == base + 1); const std::vector zeros(4, 0u); - ensure("a allocates", mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "element 0 (test)")); - ensure("b allocates", mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "element 1 (test)")); + REQUIRE(mBuffers[0].allocate(4 * sizeof(U32), 0, zeros.data(), "element 0 (test)")); + REQUIRE(mBuffers[1].allocate(4 * sizeof(U32), 0, zeros.data(), "element 1 (test)")); mCompute.bind(); mCompute.bindStorageBuffer(TEST_BLOCK_ARRAY, mBuffers[0], 0); mCompute.bindStorageBuffer(TEST_BLOCK_ARRAY, mBuffers[1], 1); mCompute.dispatch(1); - ensure_equals("element 0's buffer", readWords(mBuffers[0], 1)[0], 11u); - ensure_equals("element 1's buffer", readWords(mBuffers[1], 1)[0], 22u); + UNSCOPED_INFO("element 0's buffer"); + CHECK(readWords(mBuffers[0], 1)[0] == 11u); + UNSCOPED_INFO("element 1's buffer"); + CHECK(readWords(mBuffers[1], 1)[0] == 22u); mCompute2.bind(); mCompute2.dispatch(1); - ensure_equals("the second element's writer reaches element 1's buffer", readWords(mBuffers[1], 1)[0], 33u); - ensure_equals("and not element 0's", readWords(mBuffers[0], 1)[0], 11u); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the second element's writer reaches element 1's buffer"); + CHECK(readWords(mBuffers[1], 1)[0] == 33u); + UNSCOPED_INFO("and not element 0's"); + CHECK(readWords(mBuffers[0], 1)[0] == 11u); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // An image array takes its units in reserved order like any image, though GL names it by its first element: it is // before testImageA in the table, so its two elements take units 0 and 1 and testImageA 2. bindImage names the // element, and each target takes its element's stores. - template<> template<> - void llglslshader_object_t::test<23>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: an image array takes its units in reserved order like any image", "[llglslshader_gl]") { setUpCompute(); constexpr U32 SIDE = 8; - ensure("image array builds", - buildProgram(mCompute, "image array (test)", { { "test/imageArrayC.glsl", GL_COMPUTE_SHADER } })); - ensure_equals("element 0 first", mCompute.getImageUnit(TEST_IMAGES, 0), 0); - ensure_equals("element 1 next", mCompute.getImageUnit(TEST_IMAGES, 1), 1); - ensure_equals("the single image after the array", mCompute.getImageUnit(TEST_IMAGE_A), 2); + REQUIRE(buildProgram(mCompute, "image array (test)", { { "test/imageArrayC.glsl", GL_COMPUTE_SHADER } })); + UNSCOPED_INFO("element 0 first"); + CHECK(mCompute.getImageUnit(TEST_IMAGES, 0) == 0); + UNSCOPED_INFO("element 1 next"); + CHECK(mCompute.getImageUnit(TEST_IMAGES, 1) == 1); + UNSCOPED_INFO("the single image after the array"); + CHECK(mCompute.getImageUnit(TEST_IMAGE_A) == 2); for (U32 i = 0; i < 3; ++i) { - ensure("target allocates", mTargets[i].allocate(SIDE, SIDE, GL_R32F)); + CAPTURE(i); + REQUIRE(mTargets[i].allocate(SIDE, SIDE, GL_R32F)); } mCompute.bind(); - ensure_equals("A bound", mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY), 2); - ensure_equals("element 0 bound", mCompute.bindImage(TEST_IMAGES, &mTargets[1], ALImageSlot::WRITE_ONLY, 0, 0, 0), 0); - ensure_equals("element 1 bound", mCompute.bindImage(TEST_IMAGES, &mTargets[2], ALImageSlot::WRITE_ONLY, 0, 0, 1), 1); + CHECK(mCompute.bindImage(TEST_IMAGE_A, &mTargets[0], ALImageSlot::WRITE_ONLY) == 2); + CHECK(mCompute.bindImage(TEST_IMAGES, &mTargets[1], ALImageSlot::WRITE_ONLY, 0, 0, 0) == 0); + CHECK(mCompute.bindImage(TEST_IMAGES, &mTargets[2], ALImageSlot::WRITE_ONLY, 0, 0, 1) == 1); mCompute.dispatchThreads(SIDE, SIDE); gGL.memoryBarrier(LLRender::BARRIER_TEXTURE_UPDATE); mCompute.unbindImage(TEST_IMAGE_A); @@ -1824,23 +1875,22 @@ namespace tut const std::vector held = readTexture(mTargets[t].getTexture(), SIDE, SIDE, 1, GL_RED, GL_FLOAT); for (U32 i = 0; i < SIDE * SIDE; ++i) { - ensure_equals("target " + std::to_string(t) + " at " + std::to_string(i), held[i], expected[t]); + CAPTURE(t, i); + REQUIRE(held[i] == expected[t]); } } - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // The debug validator reads image units as it reads texture units: a signed integer image over a float format is // undefined and refused, over a signed integer one passed, and an empty unit is fine. - template<> template<> - void llglslshader_object_t::test<24>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: the debug validator reads image units as it reads texture units", "[llglslshader_gl]") { setUpCompute(); constexpr U32 SIDE = 4; - ensure("signed image builds", - buildProgram(mCompute, "int image (test)", { { "test/imageIntC.glsl", GL_COMPUTE_SHADER } })); - ensure("float target allocates", mTargets[0].allocate(SIDE, SIDE, GL_R32F)); - ensure("signed target allocates", mTargets[1].allocate(SIDE, SIDE, GL_R32I)); + REQUIRE(buildProgram(mCompute, "int image (test)", { { "test/imageIntC.glsl", GL_COMPUTE_SHADER } })); + REQUIRE(mTargets[0].allocate(SIDE, SIDE, GL_R32F)); + REQUIRE(mTargets[1].allocate(SIDE, SIDE, GL_R32I)); mCompute.bind(); gDebugGL = true; @@ -1852,29 +1902,29 @@ namespace tut gDebugGL = false; mCompute.unbindImage(TEST_IMAGE_INT); - ensure("an empty image unit passes", empty_valid); - ensure("a signed image over a float format is refused", !float_valid); - ensure("over a signed format it passes", signed_valid); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("an empty image unit passes"); + CHECK(empty_valid); + UNSCOPED_INFO("a signed image over a float format is refused"); + CHECK_FALSE(float_valid); + UNSCOPED_INFO("over a signed format it passes"); + CHECK(signed_valid); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // A depth-stencil texture reads as its depth until GL 4.3's stencil texturing turns it to its stencil: the debug // validator refuses an unsigned sampler over it as depth and passes it as stencil, and the reverse for a float // sampler. - template<> template<> - void llglslshader_object_t::test<25>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: the debug validator reads a depth-stencil texture by its texture mode", "[llglslshader_gl]") { setUpShaders(); if (gGLManager.mGLVersion < 4.29f) { - skip("the context has no stencil texturing"); + SKIP("the context has no stencil texturing"); } - ensure("unsigned reader builds", - buildProgram(mDraw, "stencil read (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); - ensure("float reader builds", - buildProgram(mDraw2, "depth read (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw, "stencil read (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/intSampleF.glsl", GL_FRAGMENT_SHADER } })); + REQUIRE(buildProgram(mDraw2, "depth read (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/sampleF.glsl", GL_FRAGMENT_SHADER } })); GLuint texture = 0; glGenTextures(1, &texture); @@ -1901,36 +1951,43 @@ namespace tut return valid; }; - ensure("an unsigned sampler over the depth refused", - !validates(mDraw, TEST_UINT_MAP, GL_DEPTH_COMPONENT)); - ensure("over the stencil passed", validates(mDraw, TEST_UINT_MAP, GL_STENCIL_INDEX)); - ensure("a float sampler over the stencil refused", !validates(mDraw2, TEST_SOURCE_MAP, GL_STENCIL_INDEX)); - ensure("over the depth passed", validates(mDraw2, TEST_SOURCE_MAP, GL_DEPTH_COMPONENT)); + UNSCOPED_INFO("an unsigned sampler over the depth refused"); + CHECK_FALSE(validates(mDraw, TEST_UINT_MAP, GL_DEPTH_COMPONENT)); + UNSCOPED_INFO("over the stencil passed"); + CHECK(validates(mDraw, TEST_UINT_MAP, GL_STENCIL_INDEX)); + UNSCOPED_INFO("a float sampler over the stencil refused"); + CHECK_FALSE(validates(mDraw2, TEST_SOURCE_MAP, GL_STENCIL_INDEX)); + UNSCOPED_INFO("over the depth passed"); + CHECK(validates(mDraw2, TEST_SOURCE_MAP, GL_DEPTH_COMPONENT)); glDeleteTextures(1, &texture); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } // Every kind of sampler takes a texture unit, in reserved order, an array included under its bare name though GL // names it by its first element: the two-element array first, then the array texture, the unsigned sampler and // the integer buffer. - template<> template<> - void llglslshader_object_t::test<26>() + TEST_CASE_METHOD(llglslshader_data, "llglslshader_gl: every kind of sampler takes a texture unit in reserved order", "[llglslshader_gl]") { setUpShaders(); - ensure("sampler types build", - buildProgram(mDraw, "sampler types (test)", - { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/samplerTypesF.glsl", GL_FRAGMENT_SHADER } })); - ensure_equals("the array first", mDraw.getTextureChannel(TEST_SAMPLERS), 0); - ensure_equals("the array texture after its two units", mDraw.getTextureChannel(TEST_SOURCE_MAP), 2); - ensure_equals("the unsigned sampler next", mDraw.getTextureChannel(TEST_UINT_MAP), 3); - ensure_equals("the integer buffer last", mDraw.getTextureChannel(TEST_INT_MAP), 4); + REQUIRE(buildProgram(mDraw, "sampler types (test)", + { { "test/fullscreenV.glsl", GL_VERTEX_SHADER }, { "test/samplerTypesF.glsl", GL_FRAGMENT_SHADER } })); + UNSCOPED_INFO("the array first"); + CHECK(mDraw.getTextureChannel(TEST_SAMPLERS) == 0); + UNSCOPED_INFO("the array texture after its two units"); + CHECK(mDraw.getTextureChannel(TEST_SOURCE_MAP) == 2); + UNSCOPED_INFO("the unsigned sampler next"); + CHECK(mDraw.getTextureChannel(TEST_UINT_MAP) == 3); + UNSCOPED_INFO("the integer buffer last"); + CHECK(mDraw.getTextureChannel(TEST_INT_MAP) == 4); GLint units[2] = { -1, -1 }; glGetUniformiv(mDraw.mProgramObject, glGetUniformLocation(mDraw.mProgramObject, "testSamplers[1]"), &units[1]); glGetUniformiv(mDraw.mProgramObject, glGetUniformLocation(mDraw.mProgramObject, "testSamplers[0]"), &units[0]); - ensure_equals("the array's first element at its unit", units[0], 0); - ensure_equals("its second at the next", units[1], 1); - ensure_equals("no GL error", glGetError(), GLenum(GL_NO_ERROR)); + UNSCOPED_INFO("the array's first element at its unit"); + CHECK(units[0] == 0); + UNSCOPED_INFO("its second at the next"); + CHECK(units[1] == 1); + CHECK(glGetError() == GLenum(GL_NO_ERROR)); } } diff --git a/indra/llrender/tests/llheadlessgl_fixture.h b/indra/llrender/tests/llheadlessgl_fixture.h index 4069eb5ed4..52f9771560 100644 --- a/indra/llrender/tests/llheadlessgl_fixture.h +++ b/indra/llrender/tests/llheadlessgl_fixture.h @@ -56,6 +56,11 @@ #include "llfile.h" #include "lluuid.h" +#if AL_TEST_CATCH2 +#include "alteststateguard.h" +#include "altestteardown.h" +#endif + #include #include #include @@ -320,7 +325,8 @@ namespace ll_test // A white texel where the viewer puts its white image, which an unbound // texture slot samples: what is drawn with no texture then comes out in // its own colour, as it does in the viewer, rather than black. Made once, - // on the first call; the context must be up. + // on the first call; the context must be up. A Catch2 binary releases it + // after the last case, before the shared context it was made in ends. inline void installWhiteTexture() { static LLPointer white; @@ -329,6 +335,9 @@ namespace ll_test LLPointer raw = new LLImageRaw(1, 1, 4); std::memset(raw->getData(), 0xFF, 4); white = new LLImageGL(raw, false); +#if AL_TEST_CATCH2 + ll_test::Teardown::add([] { white = nullptr; }); +#endif } ALTextureSlot::sWhiteTexture = white->getTexName(); // Bound once, so that the slot holds a texture: an unbind puts the @@ -541,13 +550,33 @@ namespace ll_test // tests need, so each must leave the image, render and shader state as // it found it. Tests that make a HeadlessGL of their own per test end // the GL classes under it, and are never linked beside a caller of this. + // + // A Catch2 binary ends it after the last case, while APR and LLError are + // still up, rather than at static destruction. What making it changes -- + // the context, the bound program, the matrices pushed, and whatever the + // driver chooses for the process -- is the state the guard compares with + // from then on, so the case that first asked is not blamed for it. inline HeadlessGL& sharedHeadlessGL() { +#if AL_TEST_CATCH2 + static HeadlessGL* gl = [] + { + HeadlessGL* made = new HeadlessGL(/*needs_vbos=*/true, + /*needs_imagegl=*/true, + /*needs_llrender=*/true, + /*needs_render=*/true); + ll_test::Teardown::add([made] { delete made; }); + ll_test::StateGuard::accept(); + return made; + }(); + return *gl; +#else static HeadlessGL gl(/*needs_vbos=*/true, /*needs_imagegl=*/true, /*needs_llrender=*/true, /*needs_render=*/true); return gl; +#endif } // A test's hold on the shared context. It takes sharedHeadlessGL() as it diff --git a/indra/llrender/tests/llimagegl_test.cpp b/indra/llrender/tests/llimagegl_test.cpp index ebf4734f0e..98f1771776 100644 --- a/indra/llrender/tests/llimagegl_test.cpp +++ b/indra/llrender/tests/llimagegl_test.cpp @@ -40,7 +40,7 @@ #include "llimage.h" -#include "../test/lltut.h" +#include #include #include @@ -50,8 +50,6 @@ #include #include -namespace tut -{ namespace { struct llimagegl_data @@ -98,47 +96,34 @@ namespace } }; - typedef test_group llimagegl_test; - typedef llimagegl_test::object llimagegl_object; - tut::llimagegl_test llimagegl_testcase("LLImageGL"); -} // createGLTexture(LLImageRaw) allocates immutable storage under a // fresh GL name. After it succeeds the instance reports // getHasGLTexture() and a non-zero texname. - template<> template<> - void llimagegl_object::test<1>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: createGLTexture(LLImageRaw) makes a texture under a fresh GL name", "[LLImageGL]") { LLPointer raw = makeRaw(16, 16, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(/*discard_level=*/0, raw.get())); - ensure("texname allocated", - img->getTexName() != 0); - ensure("getHasGLTexture reports true", - img->getHasGLTexture()); + CHECK(img->createGLTexture(/*discard_level=*/0, raw.get())); + CHECK(img->getTexName() != 0); + CHECK(img->getHasGLTexture()); } // destroyGLTexture must drop the GL name. Subsequent reads see // mTexName == 0 and getHasGLTexture() == false; the LLImageGL // instance itself remains usable (could be re-uploaded). - template<> template<> - void llimagegl_object::test<2>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: destroyGLTexture drops the GL name", "[LLImageGL]") { LLPointer raw = makeRaw(16, 16, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, raw.get())); - ensure("texname allocated before destroy", - img->getTexName() != 0); + CHECK(img->createGLTexture(0, raw.get())); + CHECK(img->getTexName() != 0); img->destroyGLTexture(); - ensure_equals("texname cleared after destroy", - (S32)img->getTexName(), 0); - ensure("getHasGLTexture false after destroy", - !img->getHasGLTexture()); + CHECK((S32)img->getTexName() == 0); + CHECK_FALSE(img->getHasGLTexture()); } // Regression for 8e85c68d69 — the partial-rect setSubImage path @@ -146,13 +131,11 @@ namespace // batch flush bound to nothing (one-frame glyph flicker on cache // miss). Verify the GL texture binding survives a setSubImage that // skips unbind. - template<> template<> - void llimagegl_object::test<3>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a partial setSubImage leaves the texture bound to unit 0", "[LLImageGL]") { LLPointer raw = makeRaw(32, 32, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, raw.get())); + REQUIRE(img->createGLTexture(0, raw.get())); // Bind the new texture to unit 0 the same way the renderer // would, then snapshot what GL_TEXTURE_BINDING_2D points at. @@ -161,8 +144,7 @@ namespace GLint bound_before = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound_before); - ensure_equals("texture is bound to unit 0 before setSubImage", - (S32)bound_before, (S32)img->getTexName()); + CHECK((S32)bound_before == (S32)img->getTexName()); // Partial-rect upload — this is the path that used to // disable() the unit on its way out. The source raw must @@ -171,8 +153,7 @@ namespace // offset, so a 32x32 source matching the destination is // simplest. LLPointer patch = makeRaw(32, 32, 4, /*fill=*/0xFF); - ensure("setSubImage succeeded", - img->setSubImage(patch.get(), + CHECK(img->setSubImage(patch.get(), /*x_pos=*/4, /*y_pos=*/4, /*width=*/8, /*height=*/8, /*force_fast_update=*/true, 0, true)); @@ -180,8 +161,7 @@ namespace glActiveTexture(GL_TEXTURE0); GLint bound_after = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound_after); - ensure_equals("texture still bound to unit 0 after setSubImage", - (S32)bound_after, (S32)img->getTexName()); + CHECK((S32)bound_after == (S32)img->getTexName()); } // Regression for 444c29b161 — setExplicitFormat used to leave @@ -189,35 +169,30 @@ namespace // GL_LUMINANCE, GL_ALPHA) which then leaked into setSubImage / // readBackRaw and tripped GL_INVALID_ENUM in core. Now // resolveDeprecatedFormat rewrites them at format-set time. - template<> template<> - void llimagegl_object::test<4>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: setExplicitFormat rewrites deprecated formats and keeps modern ones", "[LLImageGL]") { if (gGLManager.mGLVersion < 3.29f) { - skip("resolveDeprecatedFormat is gated on GL >= 3.3"); + SKIP("resolveDeprecatedFormat is gated on GL >= 3.3"); return; } LLPointer la = new LLImageGL(/*usemipmaps=*/false); la->setExplicitFormat(GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE); - ensure_equals("LUMINANCE_ALPHA primary rewrites to GL_RG", - (S32)la->getPrimaryFormat(), (S32)GL_RG); + CHECK((S32)la->getPrimaryFormat() == (S32)GL_RG); LLPointer lum = new LLImageGL(/*usemipmaps=*/false); lum->setExplicitFormat(GL_LUMINANCE, GL_LUMINANCE, GL_UNSIGNED_BYTE); - ensure_equals("LUMINANCE primary rewrites to GL_RED", - (S32)lum->getPrimaryFormat(), (S32)GL_RED); + CHECK((S32)lum->getPrimaryFormat() == (S32)GL_RED); LLPointer alpha = new LLImageGL(/*usemipmaps=*/false); alpha->setExplicitFormat(GL_ALPHA, GL_ALPHA, GL_UNSIGNED_BYTE); - ensure_equals("ALPHA primary rewrites to GL_RED", - (S32)alpha->getPrimaryFormat(), (S32)GL_RED); + CHECK((S32)alpha->getPrimaryFormat() == (S32)GL_RED); // Modern formats shouldn't be touched. LLPointer rgba = new LLImageGL(/*usemipmaps=*/false); rgba->setExplicitFormat(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); - ensure_equals("RGBA primary preserved", - (S32)rgba->getPrimaryFormat(), (S32)GL_RGBA); + CHECK((S32)rgba->getPrimaryFormat() == (S32)GL_RGBA); } // Pure-CPU sanity: dataFormatBits/Bytes/Components match the @@ -225,74 +200,56 @@ namespace // multiple. These are static helpers; no GL state involved, but // they live here so the table stays close to the formats the // upload paths exercise. - template<> template<> - void llimagegl_object::test<5>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: dataFormatBits/Bytes/Components give the documented host and VRAM sizes", "[LLImageGL]") { - ensure_equals("RGBA8 -> 32 bits", - LLImageGL::dataFormatBits(GL_RGBA8), 32); - ensure_equals("RG8 -> 16 bits", - LLImageGL::dataFormatBits(GL_RG8), 16); - ensure_equals("R8 -> 8 bits", - LLImageGL::dataFormatBits(GL_R8), 8); - ensure_equals("RGBA16F -> 64 bits", - LLImageGL::dataFormatBits(GL_RGBA16F), 64); - - ensure_equals("RGBA components == 4", - LLImageGL::dataFormatComponents(GL_RGBA), 4); - ensure_equals("LUMINANCE_ALPHA components == 2", - LLImageGL::dataFormatComponents(GL_LUMINANCE_ALPHA), 2); - ensure_equals("RED components == 1", - LLImageGL::dataFormatComponents(GL_RED), 1); + CHECK(LLImageGL::dataFormatBits(GL_RGBA8) == 32); + CHECK(LLImageGL::dataFormatBits(GL_RG8) == 16); + CHECK(LLImageGL::dataFormatBits(GL_R8) == 8); + CHECK(LLImageGL::dataFormatBits(GL_RGBA16F) == 64); + + CHECK(LLImageGL::dataFormatComponents(GL_RGBA) == 4); + CHECK(LLImageGL::dataFormatComponents(GL_LUMINANCE_ALPHA) == 2); + CHECK(LLImageGL::dataFormatComponents(GL_RED) == 1); // 16x16 RGBA8 = 16*16*4 = 1024 bytes, already 4-aligned. - ensure_equals("16x16 RGBA8 = 1024 bytes", - (S32)LLImageGL::dataFormatBytes(GL_RGBA8, 16, 16), 1024); + CHECK((S32)LLImageGL::dataFormatBytes(GL_RGBA8, 16, 16) == 1024); // 5x3 R8 = 15 bytes; rounds up to 16 (4-aligned). - ensure_equals("5x3 R8 byte count rounds to 4-aligned 16", - (S32)LLImageGL::dataFormatBytes(GL_R8, 5, 3), 16); + CHECK((S32)LLImageGL::dataFormatBytes(GL_R8, 5, 3) == 16); // Host vs. VRAM split: dataFormatBits returns the tight host // layout (used for CPU upload-buffer math); dataFormatVRAMBits // returns the padded driver allocation (used for VRAM stats). // For formats drivers don't pad, the two agree. - ensure_equals("RGB8 host -> 24 bits (tight)", - LLImageGL::dataFormatBits(GL_RGB8), 24); - ensure_equals("RGB8 VRAM -> 32 bits (padded to RGBX)", - LLImageGL::dataFormatVRAMBits(GL_RGB8), 32); - ensure_equals("RGB16F host -> 48 bits (tight)", - LLImageGL::dataFormatBits(GL_RGB16F), 48); - ensure_equals("RGB16F VRAM -> 64 bits (padded to RGBA16F)", - LLImageGL::dataFormatVRAMBits(GL_RGB16F), 64); - ensure_equals("RGB32F host -> 96 bits (tight)", - LLImageGL::dataFormatBits(GL_RGB32F), 96); - ensure_equals("RGB32F VRAM -> 128 bits (padded to RGBA32F)", - LLImageGL::dataFormatVRAMBits(GL_RGB32F), 128); - ensure_equals("DEPTH_COMPONENT24 host -> 24 bits (tight)", - LLImageGL::dataFormatBits(GL_DEPTH_COMPONENT24), 24); - ensure_equals("DEPTH_COMPONENT24 VRAM -> 32 bits (padded)", - LLImageGL::dataFormatVRAMBits(GL_DEPTH_COMPONENT24), 32); + CHECK(LLImageGL::dataFormatBits(GL_RGB8) == 24); + UNSCOPED_INFO("padded to RGBX"); + CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB8) == 32); + CHECK(LLImageGL::dataFormatBits(GL_RGB16F) == 48); + UNSCOPED_INFO("padded to RGBA16F"); + CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB16F) == 64); + CHECK(LLImageGL::dataFormatBits(GL_RGB32F) == 96); + UNSCOPED_INFO("padded to RGBA32F"); + CHECK(LLImageGL::dataFormatVRAMBits(GL_RGB32F) == 128); + CHECK(LLImageGL::dataFormatBits(GL_DEPTH_COMPONENT24) == 24); + CHECK(LLImageGL::dataFormatVRAMBits(GL_DEPTH_COMPONENT24) == 32); // VRAM bytes path applies the same 4-byte alignment as the // host bytes path and routes unpadded formats through the // host table — so RGBA8 matches between the two. - ensure_equals("16x16 RGBA8 VRAM = 1024 bytes (matches host)", - (S32)LLImageGL::dataFormatVRAMBytes(GL_RGBA8, 16, 16), 1024); + UNSCOPED_INFO("matches the host bytes"); + CHECK((S32)LLImageGL::dataFormatVRAMBytes(GL_RGBA8, 16, 16) == 1024); // 16x16 RGB8: host 16*16*3 = 768; VRAM 16*16*4 = 1024. - ensure_equals("16x16 RGB8 host = 768 bytes", - (S32)LLImageGL::dataFormatBytes(GL_RGB8, 16, 16), 768); - ensure_equals("16x16 RGB8 VRAM = 1024 bytes (padded)", - (S32)LLImageGL::dataFormatVRAMBytes(GL_RGB8, 16, 16), 1024); + CHECK((S32)LLImageGL::dataFormatBytes(GL_RGB8, 16, 16) == 768); + CHECK((S32)LLImageGL::dataFormatVRAMBytes(GL_RGB8, 16, 16) == 1024); } // When no explicit format is set, createGLTexture derives one from // the raw's component count. With deprecated rewrites enabled this // means: 1 -> GL_RED, 2 -> GL_RG, 3 -> GL_RGB, 4 -> GL_RGBA. - template<> template<> - void llimagegl_object::test<6>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: createGLTexture picks the primary format from the component count", "[LLImageGL]") { if (gGLManager.mGLVersion < 3.29f) { - skip("resolveDeprecatedFormat is gated on GL >= 3.3"); + SKIP("resolveDeprecatedFormat is gated on GL >= 3.3"); return; } @@ -302,39 +259,32 @@ namespace // to 2 component"). LLPointer raw2 = makeRaw(8, 8, 2, /*fill=*/0x80); LLPointer img2 = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture 2-component succeeded", - img2->createGLTexture(0, raw2.get())); - ensure_equals("2 components -> GL_RG primary", - (S32)img2->getPrimaryFormat(), (S32)GL_RG); + CHECK(img2->createGLTexture(0, raw2.get())); + CHECK((S32)img2->getPrimaryFormat() == (S32)GL_RG); LLPointer raw4 = makeRaw(8, 8, 4, /*fill=*/0x80); LLPointer img4 = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture 4-component succeeded", - img4->createGLTexture(0, raw4.get())); - ensure_equals("4 components -> GL_RGBA primary", - (S32)img4->getPrimaryFormat(), (S32)GL_RGBA); + CHECK(img4->createGLTexture(0, raw4.get())); + CHECK((S32)img4->getPrimaryFormat() == (S32)GL_RGBA); } // usemipmaps=true must wire the auto-mipmap path through to GL — // verify by reading the bound texture's mip 1 dimensions back. Mip // 1 of an 8x8 base is 4x4; if mipmaps weren't generated the level // would have width 0. - template<> template<> - void llimagegl_object::test<7>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a texture made with usemipmaps has a mip chain in GL", "[LLImageGL]") { LLPointer raw = makeRaw(8, 8, 4, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture mipmapped succeeded", - img->createGLTexture(0, raw.get())); - ensure("getUseMipMaps reports true", - img->getUseMipMaps()); + REQUIRE(img->createGLTexture(0, raw.get())); + CHECK(img->getUseMipMaps()); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, img->getTexName()); GLint mip1_w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &mip1_w); - ensure_equals("mip 1 width == 4 for an 8x8 base", (S32)mip1_w, 4); + CHECK((S32)mip1_w == 4); } // Regression for 2d4e2a60ea — GL_TEXTURE_SWIZZLE_RGBA must be set @@ -342,12 +292,11 @@ namespace // sees (R, R, R, A) for a 2-component LUMINANCE_ALPHA upload even // though the texture is now stored as GL_RG. Verifies the swizzle // mask matches applySwizzleForDeprecatedFormat's table. - template<> template<> - void llimagegl_object::test<8>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: createGLTexture sets the RRRA swizzle for luminance-alpha", "[LLImageGL]") { if (gGLManager.mGLVersion < 3.29f) { - skip("swizzle apply is gated on GL >= 3.3"); + SKIP("swizzle apply is gated on GL >= 3.3"); return; } @@ -356,8 +305,7 @@ namespace // createGLTexture then applies the matching swizzle. LLPointer raw = makeRaw(8, 8, 2, /*fill=*/0x80); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture 2-component succeeded", - img->createGLTexture(0, raw.get())); + REQUIRE(img->createGLTexture(0, raw.get())); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, img->getTexName()); @@ -365,18 +313,18 @@ namespace GLint mask[4] = { 0, 0, 0, 0 }; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, mask); - ensure_equals("R swizzle == GL_RED", (S32)mask[0], (S32)GL_RED); - ensure_equals("G swizzle == GL_RED", (S32)mask[1], (S32)GL_RED); - ensure_equals("B swizzle == GL_RED", (S32)mask[2], (S32)GL_RED); - ensure_equals("A swizzle == GL_GREEN", (S32)mask[3], (S32)GL_GREEN); + CHECK((S32)mask[0] == (S32)GL_RED); + CHECK((S32)mask[1] == (S32)GL_RED); + CHECK((S32)mask[2] == (S32)GL_RED); + CHECK((S32)mask[3] == (S32)GL_GREEN); } // Round-trip: upload a known pattern, glGetTexImage it back, and // verify byte-for-byte. Covers the createGLTexture upload path and // readBackRaw together. RGBA chosen so no deprecated rewrite kicks - // in — that path is covered separately by test<4> and test<6>. - template<> template<> - void llimagegl_object::test<9>() + // in — that path is covered separately by the setExplicitFormat and + // component-count cases above. + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: readBackRaw returns the bytes createGLTexture uploaded", "[LLImageGL]") { constexpr U16 W = 8, H = 8; constexpr S8 C = 4; @@ -388,17 +336,14 @@ namespace sd[i] = (U8)(i & 0xFF); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, src.get())); + REQUIRE(img->createGLTexture(0, src.get())); LLPointer dst = new LLImageRaw(W, H, C); std::memset(dst->getData(), 0, (size_t)W * H * C); - ensure("readBackRaw succeeded", - img->readBackRaw(0, dst.get(), /*compressed_ok=*/false)); + CHECK(img->readBackRaw(0, dst.get(), /*compressed_ok=*/false)); - ensure_equals("readback bytes match upload", - std::memcmp(src->getData(), dst->getData(), - (size_t)W * H * C), + CHECK(std::memcmp(src->getData(), dst->getData(), + (size_t)W * H * C) == 0); } @@ -408,8 +353,7 @@ namespace // main thread on Windows off Intel, on Linux with NVIDIA, on macOS with AMD -- // stepped through rows by component count times a per-type width that had no // entry for it, and died. Where the slicing is off this passes either way. - template<> template<> - void llimagegl_object::test<10>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a packed pixel type uploads and updates as one value a pixel", "[LLImageGL]") { // 64 rows, a multiple of 32: the full upload takes the batched slices. constexpr U16 W = 64, H = 64; @@ -420,20 +364,19 @@ namespace LLPointer img = new LLImageGL(/*usemipmaps=*/false); img->setExplicitFormat(GL_RGBA8, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV); - ensure("createGLTexture succeeded", img->createGLTexture(0, src.get())); + REQUIRE(img->createGLTexture(0, src.get())); std::vector got((size_t)W * H * 4); readTexture(img, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, got.data()); - ensure_equals("full upload reads back", - std::memcmp(got.data(), sd, got.size()), 0); + UNSCOPED_INFO("the full upload"); + CHECK(std::memcmp(got.data(), sd, got.size()) == 0); // A partial-width update, which slices one row at a time. LLPointer patch = new LLImageRaw(W, H, 4); U8* pd = patch->getData(); for (size_t i = 0; i < (size_t)W * H * 4; ++i) pd[i] = (U8)(0xFF - (i & 0xFF)); - ensure("setSubImage succeeded", - img->setSubImage(patch.get(), /*x_pos=*/8, /*y_pos=*/4, + CHECK(img->setSubImage(patch.get(), /*x_pos=*/8, /*y_pos=*/4, /*width=*/16, /*height=*/8, /*force_fast_update=*/true)); @@ -448,7 +391,8 @@ namespace match = std::memcmp(got.data() + at, (in_patch ? pd : sd) + at, 4) == 0; } } - ensure("the update lands where it was asked and nowhere else", match); + UNSCOPED_INFO("the update lands where it was asked and nowhere else"); + CHECK(match); } // scaleDown builds a new texture object, and texture-object state does not @@ -456,8 +400,7 @@ namespace // back red and luminance-alpha opaque. The FBO method -- the default -- could // not have kept the channels either, since it samples THROUGH the swizzle and // copies back by position, so these take the PBO copy whatever the setting. - template<> template<> - void llimagegl_object::test<11>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: scaleDown keeps a deprecated-format texture's swizzle and channels", "[LLImageGL]") { constexpr U16 W = 16, H = 16; LLPointer raw = new LLImageRaw(W, H, 2); @@ -469,26 +412,26 @@ namespace } LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture succeeded", img->createGLTexture(0, raw.get())); + REQUIRE(img->createGLTexture(0, raw.get())); const U32 method = gGLManager.mDownScaleMethod; gGLManager.mDownScaleMethod = 0; const bool scaled = img->scaleDown(1); gGLManager.mDownScaleMethod = method; - ensure("scaleDown succeeded", scaled); - ensure_equals("discard level follows", img->getDiscardLevel(), 1); + CHECK(scaled); + CHECK(img->getDiscardLevel() == 1); bindForRead(img); GLint w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - ensure_equals("the new texture is half size", (S32)w, (S32)W / 2); + REQUIRE((S32)w == (S32)W / 2); GLint mask[4] = { 0, 0, 0, 0 }; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, mask); - ensure_equals("R swizzle == GL_RED", (S32)mask[0], (S32)GL_RED); - ensure_equals("G swizzle == GL_RED", (S32)mask[1], (S32)GL_RED); - ensure_equals("B swizzle == GL_RED", (S32)mask[2], (S32)GL_RED); - ensure_equals("A swizzle == GL_GREEN", (S32)mask[3], (S32)GL_GREEN); + CHECK((S32)mask[0] == (S32)GL_RED); + CHECK((S32)mask[1] == (S32)GL_RED); + CHECK((S32)mask[2] == (S32)GL_RED); + CHECK((S32)mask[3] == (S32)GL_GREEN); std::vector got((size_t)(W / 2) * (H / 2) * 2); glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_UNSIGNED_BYTE, got.data()); @@ -497,18 +440,17 @@ namespace { match = got[i] == 0x40 && got[i + 1] == 0xC0; } - ensure("luminance and alpha both survive the copy", match); + UNSCOPED_INFO("luminance and alpha both survive the copy"); + CHECK(match); } // scaleDown waits out an upload in flight: the LLImageGL thread is writing the // discard level and storage state it reads and replaces, and that upload's // publish would then install its texture under the discard level set here. - template<> template<> - void llimagegl_object::test<12>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: scaleDown waits until an upload in flight has ended", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); const U32 name = img->getTexName(); // The PBO copy needs no framebuffer of its own. @@ -523,9 +465,11 @@ namespace gGLManager.mDownScaleMethod = method; - ensure("refused while an upload is in flight", !during); - ensure_equals("texture untouched meanwhile", (S32)name_during, (S32)name); - ensure("done once the upload has ended", after); + UNSCOPED_INFO("refused while an upload is in flight"); + CHECK_FALSE(during); + CHECK((S32)name_during == (S32)name); + UNSCOPED_INFO("done once the upload has ended"); + CHECK(after); } // While an upload is in flight the members describe the texture being built. @@ -533,8 +477,7 @@ namespace // level, so it read the old texture's level 0 at the new texture's size. // Asset 64x64, on screen at discard 2 (a 16x16 texture), the upload in flight // building discard 0. - template<> template<> - void llimagegl_object::test<13>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: readBackRaw during an upload reads the texture on screen", "[LLImageGL]") { constexpr U16 W = 16, H = 16; LLPointer src = new LLImageRaw(W, H, 4); @@ -543,8 +486,8 @@ namespace sd[i] = (U8)(i & 0xFF); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", img->createGLTexture(2, src.get())); - ensure_equals("on screen at 16 wide", img->getWidth(), 16); + REQUIRE(img->createGLTexture(2, src.get())); + CHECK(img->getWidth() == 16); img->beginUpload(); img->setDiscardLevel(0); // what the worker's createGLTexture writes first @@ -555,116 +498,120 @@ namespace img->setDiscardLevel(2); img->endUpload(); - ensure("readBackRaw succeeded", read); - ensure_equals("read at the size on screen", (S32)dst->getWidth(), (S32)W); - ensure_equals("and reads what is on screen", - std::memcmp(dst->getData(), sd, (size_t)W * H * 4), 0); + REQUIRE(read); + UNSCOPED_INFO("read at the size on screen"); + REQUIRE((S32)dst->getWidth() == (S32)W); + UNSCOPED_INFO("and reads what is on screen"); + CHECK(std::memcmp(dst->getData(), sd, (size_t)W * H * 4) == 0); } // The pick mask of a texture still being uploaded waits for that texture: // getMask answers for the one on screen, and off the main thread building the // mask in place freed the buffer getMask was reading. It publishes with the // texture, and an upload that never publishes leaves it alone. - template<> template<> - void llimagegl_object::test<14>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: the pick mask of an upload in flight publishes with its texture", "[LLImageGL]") { constexpr U16 W = 16, H = 16; LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(W, H, 4, 0xFF).get())); + REQUIRE(img->createGLTexture(0, makeRaw(W, H, 4, 0xFF).get())); const LLVector2 centre(0.5f, 0.5f); - ensure("opaque texture picks", img->getMask(centre)); + UNSCOPED_INFO("opaque texture picks"); + CHECK(img->getMask(centre)); LLPointer clear = makeRaw(W, H, 4, 0x00); img->beginUpload(); img->updatePickMask(W, H, clear->getData()); - ensure("the mask on screen answers while the upload is in flight", - img->getMask(centre)); + UNSCOPED_INFO("the mask on screen answers while the upload is in flight"); + CHECK(img->getMask(centre)); img->endUpload(); - ensure("an upload that never published leaves the mask alone", - img->getMask(centre)); + UNSCOPED_INFO("an upload that never published leaves the mask alone"); + CHECK(img->getMask(centre)); img->beginUpload(); img->updatePickMask(W, H, clear->getData()); img->syncTexName(img->getTexName()); - ensure("the new mask publishes with its texture", !img->getMask(centre)); + UNSCOPED_INFO("the new mask publishes with its texture"); + CHECK_FALSE(img->getMask(centre)); } // An edit writes through the ACTIVE unit, and a bind that finds the texture // already cached on slot 0 skips activating it. With another unit left active // the write went to whatever that unit held. The glyph atlas does exactly this: // still on slot 0 from the text being drawn when a new glyph arrives. - template<> template<> - void llimagegl_object::test<15>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: an edit reaches its own texture while another unit is active", "[LLImageGL]") { constexpr U16 W = 32, H = 32; LLPointer a = new LLImageGL(/*usemipmaps=*/false); LLPointer b = new LLImageGL(/*usemipmaps=*/false); - ensure("create a", a->createGLTexture(0, makeRaw(W, H, 4, 0x10).get())); - ensure("create b", b->createGLTexture(0, makeRaw(W, H, 4, 0x20).get())); + REQUIRE(a->createGLTexture(0, makeRaw(W, H, 4, 0x10).get())); + REQUIRE(b->createGLTexture(0, makeRaw(W, H, 4, 0x20).get())); // Partial update, as the glyph atlas does it. Forcing the slot 1 bind is what // leaves unit 1 active, cached or not. gGL.getTextureSlot(0)->bind(a.get()); gGL.getTextureSlot(1)->bind(b.get(), false, /*forceBind=*/true); - ensure("setSubImage succeeded", - a->setSubImage(makeRaw(W, H, 4, 0xFF).get(), 4, 4, 8, 8, + CHECK(a->setSubImage(makeRaw(W, H, 4, 0xFF).get(), 4, 4, 8, 8, /*force_fast_update=*/true)); - ensure_equals("the update reached a", readTexelRGBA(a, 5, 5), 0xFFFFFFFFu); - ensure_equals("and not b", readTexelRGBA(b, 5, 5), 0x20202020u); + UNSCOPED_INFO("the update reached a"); + CHECK(readTexelRGBA(a, 5, 5) == 0xFFFFFFFFu); + UNSCOPED_INFO("the update left b alone"); + CHECK(readTexelRGBA(b, 5, 5) == 0x20202020u); // Re-upload in place, through setImage. gGL.getTextureSlot(0)->bind(a.get()); gGL.getTextureSlot(1)->bind(b.get(), false, /*forceBind=*/true); - ensure("re-upload succeeded", a->createGLTexture(0, makeRaw(W, H, 4, 0x77).get())); - ensure_equals("the re-upload reached a", readTexelRGBA(a, 5, 5), 0x77777777u); - ensure_equals("and not b", readTexelRGBA(b, 5, 5), 0x20202020u); + REQUIRE(a->createGLTexture(0, makeRaw(W, H, 4, 0x77).get())); + UNSCOPED_INFO("the re-upload reached a"); + CHECK(readTexelRGBA(a, 5, 5) == 0x77777777u); + UNSCOPED_INFO("the re-upload left b alone"); + CHECK(readTexelRGBA(b, 5, 5) == 0x20202020u); } // A re-upload at the same discard level wrote into the live texture in place, // on the word of the discard level alone. Immutable storage cannot follow a new // size or format: the write was rejected, or dropped the channel that did not // fit, and the image went on describing a texture GL never had. - template<> template<> - void llimagegl_object::test<16>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a re-upload at the same discard level takes a new size or format", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("create 32x32", img->createGLTexture(0, makeRaw(32, 32, 4, 0x11).get())); - ensure("re-create 64x64", img->createGLTexture(0, makeRaw(64, 64, 4, 0x22).get())); + REQUIRE(img->createGLTexture(0, makeRaw(32, 32, 4, 0x11).get())); + REQUIRE(img->createGLTexture(0, makeRaw(64, 64, 4, 0x22).get())); bindForRead(img); GLint w = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - ensure_equals("the texture took the new size", (S32)w, 64); - ensure_equals("and the new pixels", readTexelRGBA(img, 40, 40), 0x22222222u); + UNSCOPED_INFO("the texture took the new size"); + CHECK((S32)w == 64); + UNSCOPED_INFO("the resized texture holds the new pixels"); + CHECK(readTexelRGBA(img, 40, 40) == 0x22222222u); // Same size, an alpha channel the old storage has no room for. LLPointer rgb = new LLImageGL(/*usemipmaps=*/false); - ensure("create RGB", rgb->createGLTexture(0, makeRaw(16, 16, 3, 0x33).get())); - ensure("re-create RGBA", rgb->createGLTexture(0, makeRaw(16, 16, 4, 0x44).get())); + REQUIRE(rgb->createGLTexture(0, makeRaw(16, 16, 3, 0x33).get())); + REQUIRE(rgb->createGLTexture(0, makeRaw(16, 16, 4, 0x44).get())); bindForRead(rgb); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("the texture took the new format", (S32)format, (S32)GL_SRGB8_ALPHA8); - ensure_equals("and the new pixels", readTexelRGBA(rgb, 3, 3), 0x44444444u); + UNSCOPED_INFO("the texture took the new format"); + CHECK((S32)format == (S32)GL_SRGB8_ALPHA8); + UNSCOPED_INFO("the reformatted texture holds the new pixels"); + CHECK(readTexelRGBA(rgb, 3, 3) == 0x44444444u); } // A deleted texture's name may come back out of glGenTextures, and a slot whose // bind cache still held it would take the new texture for bound already and skip // the bind -- sampling texture 0 in its place, since deleting the old one put the // binding back to 0. The cache forgets the name when the texture goes. - template<> template<> - void llimagegl_object::test<17>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a slot's bind cache forgets a deleted texture's name", "[LLImageGL]") { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); const U32 name = img->getTexName(); gGL.getTextureSlot(3)->bind(img.get()); - ensure_equals("slot 3 caches it", gGL.getTextureSlot(3)->getCurrTexture(), name); + CHECK(gGL.getTextureSlot(3)->getCurrTexture() == name); img->destroyGLTexture(); // However many frames the delete is held back for. @@ -673,44 +620,45 @@ namespace LLImageGL::updateClass(); } - ensure_equals("the cache let go of the deleted name", - gGL.getTextureSlot(3)->getCurrTexture(), 0u); + CHECK(gGL.getTextureSlot(3)->getCurrTexture() == 0u); } // Generated mips fill the whole pyramid the storage holds, and sampling can // reach it. MAX_LEVEL stayed at the discard levels -- five below 256x256 -- so // glGenerateMipmap stopped there and distant surfaces minified no further. - template<> template<> - void llimagegl_object::test<18>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: generated mips fill the whole pyramid and sampling reaches it", "[LLImageGL]") { constexpr U16 W = 256, H = 256; LLPointer img = new LLImageGL(/*usemipmaps=*/true); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(W, H, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(W, H, 4, 0x80).get())); bindForRead(img); GLint max_level = 0; glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, &max_level); const S32 last = LLImageGL::calcMipLevelCount(W, H) - 1; - ensure_equals("sampling reaches the 1x1 level", (S32)max_level, last); + UNSCOPED_INFO("sampling reaches the 1x1 level"); + CHECK((S32)max_level == last); U8 px[4] = { 0, 0, 0, 0 }; glGetTexImage(GL_TEXTURE_2D, last, GL_RGBA, GL_UNSIGNED_BYTE, px); - ensure("and the 1x1 level was generated", - px[0] == 0x80 && px[1] == 0x80 && px[2] == 0x80 && px[3] == 0x80); + { + INFO("the 1x1 level was generated"); + CHECK(px[0] == 0x80); + CHECK(px[1] == 0x80); + CHECK(px[2] == 0x80); + CHECK(px[3] == 0x80); + } } // gGL is thread_local and outlives a context, as it does across these tests, // and init left the slots' bind caches as the last context had them. A fresh // context hands out the same first names, so its first texture read as bound // already on a slot that had held its namesake, and the bind was skipped. - template<> template<> - void llimagegl_object::test<19>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context starts with clear bind caches and unit 0 active", "[LLImageGL]") { { LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x80).get())); gGL.getTextureSlot(2)->bind(img.get()); } @@ -718,26 +666,23 @@ namespace gl.reset(); gl = std::make_unique(); - ensure_equals("unit 2's cache starts empty", - gGL.getTextureSlot(2)->getCurrTexture(), 0u); - ensure_equals("and unit 0 is the active one", - gGL.getCurrentTexUnitIndex(), 0u); + CHECK(gGL.getTextureSlot(2)->getCurrTexture() == 0u); + CHECK(gGL.getCurrentTexUnitIndex() == 0u); LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded in the new context", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x40).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x40).get())); gGL.getTextureSlot(2)->bind(img.get()); GLint bound = 0; glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound); - ensure_equals("the bind reached GL", (U32)bound, img->getTexName()); + UNSCOPED_INFO("the bind reached GL"); + CHECK((U32)bound == img->getTexName()); } // The same for the blend and colour-mask caches. A context left blending // BT_ALPHA made init's own setSceneBlendType(BT_ALPHA) look redundant, so the // next context kept GL's ONE, ZERO while gGL believed it was alpha blending; // and a mask left off read as still off on a context writing every channel. - template<> template<> - void llimagegl_object::test<20>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context's blend and colour-mask caches match GL", "[LLImageGL]") { gGL.setSceneBlendType(LLRender::BT_ALPHA); gGL.setColorMask(false, false); @@ -750,27 +695,37 @@ namespace GLint dst = 0; glGetIntegerv(GL_BLEND_SRC_RGB, &src); glGetIntegerv(GL_BLEND_DST_RGB, &dst); - ensure_equals("init's alpha blending reached GL: source", - (S32)src, (S32)GL_SRC_ALPHA); - ensure_equals("init's alpha blending reached GL: destination", - (S32)dst, (S32)GL_ONE_MINUS_SRC_ALPHA); + { + INFO("init's alpha blending reached GL"); + CHECK((S32)src == (S32)GL_SRC_ALPHA); + CHECK((S32)dst == (S32)GL_ONE_MINUS_SRC_ALPHA); + } bool mask[4] = { false, false, false, false }; gGL.getColorMask(mask); - ensure("the colour mask cache says what GL starts with", - mask[0] && mask[1] && mask[2] && mask[3]); + { + INFO("the colour mask cache says what GL starts with"); + CHECK(mask[0]); + CHECK(mask[1]); + CHECK(mask[2]); + CHECK(mask[3]); + } GLboolean gl_mask[4] = { GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE }; glGetBooleanv(GL_COLOR_WRITEMASK, gl_mask); - ensure("and GL agrees", - gl_mask[0] && gl_mask[1] && gl_mask[2] && gl_mask[3]); + { + INFO("and GL's mask agrees"); + CHECK(gl_mask[0]); + CHECK(gl_mask[1]); + CHECK(gl_mask[2]); + CHECK(gl_mask[3]); + } } // And for vertex buffer names, which LLVertexBuffer pools per thread as // LLImageGL pools texture names. The pool was never emptied with its context, // so the next context bound a name it had never generated -- which a core // profile refuses -- and the buffer was never allocated. - template<> template<> - void llimagegl_object::test<21>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context binds vertex buffer names it made itself", "[LLImageGL]") { // A buffer of three vertices, written and unmapped as a caller does. Where the // name comes from the pool differs: the default VBO pool draws it when it @@ -779,9 +734,10 @@ namespace // after the unmap is the name this buffer drew from the pool. auto fill = [](LLVertexBuffer* vb, const std::string& when) { - ensure("allocateBuffer succeeded " + when, vb->allocateBuffer(3, 0)); + CAPTURE(when); + REQUIRE(vb->allocateBuffer(3, 0)); LLStrider verts; - ensure("getVertexStrider succeeded " + when, vb->getVertexStrider(verts)); + REQUIRE(vb->getVertexStrider(verts)); for (S32 i = 0; i < 3; ++i) { verts[i] = LLVector3((F32)i, 0.f, 0.f); @@ -803,8 +759,9 @@ namespace fill(vb, "in the new context"); GLint bound = 0; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &bound); - ensure("a buffer is bound", bound != 0); - ensure("and it is one this context made", glIsBuffer((GLuint)bound) == GL_TRUE); + CHECK(bound != 0); + UNSCOPED_INFO("it is a buffer this context made"); + CHECK(glIsBuffer((GLuint)bound) == GL_TRUE); LLVertexBuffer::unbind(); } @@ -812,8 +769,7 @@ namespace // out the GPU: it fences, hands over, and moves on, and the main thread // publishes the texture once the fence has signalled. Until then the image // goes on showing the texture it had, and the caller's completion waits. - template<> template<> - void llimagegl_object::test<22>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: an upload on the LLImageGL thread publishes once its fence signals", "[LLImageGL]") { // The queue the upload thread hands its results back on, as the viewer's. // It has to exist before the image, which looks it up when built. @@ -830,8 +786,7 @@ namespace } thread_end; LLPointer img = new LLImageGL(/*usemipmaps=*/false); - ensure("createGLTexture succeeded", - img->createGLTexture(0, makeRaw(16, 16, 4, 0x11).get())); + REQUIRE(img->createGLTexture(0, makeRaw(16, 16, 4, 0x11).get())); const U32 old_name = img->getTexName(); LLPointer raw = makeRaw(32, 32, 4, 0x55); @@ -853,8 +808,9 @@ namespace } // The handover was posted before worker_done was raised. mainloop.runPending(); - ensure("the worker finished", worker_done.load()); - ensure("and its createGLTexture succeeded", worker_ok.load()); + CHECK(worker_done.load()); + UNSCOPED_INFO("the worker's createGLTexture succeeded"); + CHECK(worker_ok.load()); bool completed = false; U32 name_at_completion = 0; @@ -864,9 +820,12 @@ namespace name_at_completion = img->getTexName(); }); - ensure("not published before its fence is seen", !completed); - ensure_equals("the texture on screen is the old one", img->getTexName(), old_name); - ensure_equals("at its own size", img->getWidth(), 16); + UNSCOPED_INFO("not published before its fence is seen"); + CHECK_FALSE(completed); + UNSCOPED_INFO("the texture on screen is the old one"); + CHECK(img->getTexName() == old_name); + UNSCOPED_INFO("still at the old size"); + CHECK(img->getWidth() == 16); while (LLImageGL::publishUploads() > 0 && std::chrono::steady_clock::now() < deadline) { @@ -875,18 +834,22 @@ namespace LLImageGLThread::deleteSingleton(); - ensure("the completion ran", completed); - ensure("after the new texture published", name_at_completion != old_name); - ensure_equals("which is what the image names", img->getTexName(), name_at_completion); - ensure_equals("at its new size", img->getWidth(), 32); - ensure_equals("holding the upload", readTexelRGBA(img, 5, 5), 0x55555555u); + UNSCOPED_INFO("the afterPublish completion ran"); + CHECK(completed); + UNSCOPED_INFO("after the new texture published"); + CHECK(name_at_completion != old_name); + UNSCOPED_INFO("the image names the published texture"); + CHECK(img->getTexName() == name_at_completion); + UNSCOPED_INFO("at the new size"); + CHECK(img->getWidth() == 32); + UNSCOPED_INFO("holding the upload's pixels"); + CHECK(readTexelRGBA(img, 5, 5) == 0x55555555u); } // And the rest of what LLRender's setters cache. A polygon offset or patch // size the last context was left with made the same setting on the next one // look redundant, so the fresh context kept GL's initial value instead. - template<> template<> - void llimagegl_object::test<23>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a new context takes the polygon offset and patch size set on it", "[LLImageGL]") { gGL.setPolygonOffset(1.f, 2.f); gGL.setPatchVertices(4); @@ -903,19 +866,18 @@ namespace GLfloat units = 0.f; glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor); glGetFloatv(GL_POLYGON_OFFSET_UNITS, &units); - ensure_equals("the polygon offset reached the new context: factor", (F32)factor, sign * 1.f); - ensure_equals("the polygon offset reached the new context: units", (F32)units, sign * 2.f); + CHECK((F32)factor == sign * 1.f); + CHECK((F32)units == sign * 2.f); GLint patch = 0; glGetIntegerv(GL_PATCH_VERTICES, &patch); - ensure_equals("and the patch size", (S32)patch, 4); + CHECK((S32)patch == 4); } // Which vertex attribute arrays are enabled is vertex-array-object state, and // a fresh context's VAO has none. LLVertexBuffer's cache of them outlived the // context, so the next context's first draw skipped enabling the arrays it read. - template<> template<> - void llimagegl_object::test<24>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: setupClientArrays enables the position array again on a new context", "[LLImageGL]") { LLVertexBuffer::setupClientArrays(LLVertexBuffer::MAP_VERTEX); @@ -928,19 +890,18 @@ namespace glGetVertexAttribiv(LLVertexBuffer::TYPE_VERTEX, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &enabled); LLVertexBuffer::setupClientArrays(0); - ensure("the position array is enabled on the new context", enabled == GL_TRUE); + CHECK(enabled == GL_TRUE); } // A worker ends by destroying its shared context on its own thread, where // it is current, as LLImageGLThread does. On EGL that only marked it for // destruction, and the context held its display past the window, so the // next window was made on the stale display, and failed. - template<> template<> - void llimagegl_object::test<25>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a window can be made after a worker destroyed its shared context", "[LLImageGL]") { LLWindow* window = gl->window(); void* shared = window->createSharedContext(); - ensure("a shared context was made", shared != nullptr); + REQUIRE(shared != nullptr); std::thread worker([window, shared] { window->makeContextCurrent(shared); @@ -955,15 +916,14 @@ namespace } catch (const std::exception& e) { - fail(std::string("the next window: ") + e.what()); + FAIL(std::string("the next window: ") + e.what()); } } // The integer formats through LLImageGL. R32UI, RG16I and RGBA32UI allocate as themselves, hold what was uploaded // bit for bit -- past 2^24, and below 0 -- and take a partial update; each is accounted at its texels' size; and // the one asked for with mips holds level 0 alone, since GL neither builds nor filters an integer texture's mips. - template<> template<> - void llimagegl_object::test<26>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: integer formats allocate as themselves and hold their texels exactly", "[LLImageGL]") { constexpr U16 W = 13, H = 7; constexpr size_t N = (size_t)W * H; @@ -976,23 +936,25 @@ namespace LLPointer r32ui = new LLImageGL(W, H, 1, /*usemipmaps=*/true); r32ui->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); U64 before = LLImageGL::getTextureBytesAllocated(); - ensure("R32UI created", r32ui->createGLTexture(0, reinterpret_cast(uints.data()))); - ensure_equals("R32UI accounted at four bytes a texel", - (S64)(LLImageGL::getTextureBytesAllocated() - before), (S64)N * 4); - ensure_equals("and its host bytes", (S64)r32ui->getMipBytes(0), (S64)N * 4); - ensure("no mips asked of it", !r32ui->getUseMipMaps()); + REQUIRE(r32ui->createGLTexture(0, reinterpret_cast(uints.data()))); + UNSCOPED_INFO("R32UI accounted at four bytes a texel"); + CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 4); + CHECK((S64)r32ui->getMipBytes(0) == (S64)N * 4); + UNSCOPED_INFO("an integer texture asked for mips takes none"); + CHECK_FALSE(r32ui->getUseMipMaps()); bindForRead(r32ui); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("R32UI storage", (S32)format, (S32)GL_R32UI); + CHECK((S32)format == (S32)GL_R32UI); GLint level1_width = -1; glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &level1_width); - ensure_equals("no level past the first", (S32)level1_width, 0); + UNSCOPED_INFO("no level past the first"); + CHECK((S32)level1_width == 0); std::vector got_uints(N, 0); readTexture(r32ui, GL_RED_INTEGER, GL_UNSIGNED_INT, got_uints.data()); - ensure("R32UI holds the upload", got_uints == uints); + CHECK(got_uints == uints); // A partial update, its rows read from a wider source as the glyph atlas updates. std::vector patch(N); @@ -1000,8 +962,7 @@ namespace { patch[i] = 0xF0000000u + (U32)i; } - ensure("R32UI partial update", - r32ui->setSubImage(reinterpret_cast(patch.data()), W, H, 2, 1, 4, 3, /*force_fast_update=*/true)); + CHECK(r32ui->setSubImage(reinterpret_cast(patch.data()), W, H, 2, 1, 4, 3, /*force_fast_update=*/true)); readTexture(r32ui, GL_RED_INTEGER, GL_UNSIGNED_INT, got_uints.data()); for (U32 y = 0; y < H; ++y) { @@ -1009,8 +970,9 @@ namespace { const size_t i = (size_t)y * W + x; const bool in_patch = x >= 2 && x < 6 && y >= 1 && y < 4; - ensure_equals("R32UI texel " + std::to_string(x) + "," + std::to_string(y), got_uints[i], - in_patch ? patch[i] : uints[i]); + CAPTURE(x, y); + REQUIRE(got_uints[i] == + (in_patch ? patch[i] : uints[i])); } } @@ -1023,15 +985,15 @@ namespace LLPointer rg16i = new LLImageGL(W, H, 2, /*usemipmaps=*/false); rg16i->setExplicitFormat(GL_RG16I, GL_RG_INTEGER, GL_SHORT); before = LLImageGL::getTextureBytesAllocated(); - ensure("RG16I created", rg16i->createGLTexture(0, reinterpret_cast(shorts.data()))); - ensure_equals("RG16I accounted at four bytes a texel", - (S64)(LLImageGL::getTextureBytesAllocated() - before), (S64)N * 4); + REQUIRE(rg16i->createGLTexture(0, reinterpret_cast(shorts.data()))); + UNSCOPED_INFO("RG16I accounted at four bytes a texel"); + CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 4); bindForRead(rg16i); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("RG16I storage", (S32)format, (S32)GL_RG16I); + CHECK((S32)format == (S32)GL_RG16I); std::vector got_shorts(N * 2, 0); readTexture(rg16i, GL_RG_INTEGER, GL_SHORT, got_shorts.data()); - ensure("RG16I holds the upload", got_shorts == shorts); + CHECK(got_shorts == shorts); std::vector wide(N * 4); for (size_t i = 0; i < N; ++i) @@ -1044,28 +1006,27 @@ namespace LLPointer rgba32ui = new LLImageGL(W, H, 4, /*usemipmaps=*/false); rgba32ui->setExplicitFormat(GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT); before = LLImageGL::getTextureBytesAllocated(); - ensure("RGBA32UI created", rgba32ui->createGLTexture(0, reinterpret_cast(wide.data()))); - ensure_equals("RGBA32UI accounted at sixteen bytes a texel", - (S64)(LLImageGL::getTextureBytesAllocated() - before), (S64)N * 16); + REQUIRE(rgba32ui->createGLTexture(0, reinterpret_cast(wide.data()))); + UNSCOPED_INFO("RGBA32UI accounted at sixteen bytes a texel"); + CHECK((S64)(LLImageGL::getTextureBytesAllocated() - before) == (S64)N * 16); bindForRead(rgba32ui); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("RGBA32UI storage", (S32)format, (S32)GL_RGBA32UI); + CHECK((S32)format == (S32)GL_RGBA32UI); std::vector got_wide(N * 4, 0); readTexture(rgba32ui, GL_RGBA_INTEGER, GL_UNSIGNED_INT, got_wide.data()); - ensure("RGBA32UI holds the upload", got_wide == wide); + CHECK(got_wide == wide); // What an LLImageRaw cannot hold, readBackRaw does not read: it has a byte a component. LLPointer raw = new LLImageRaw(); - ensure("no byte image of R32UI", !r32ui->readBackRaw(0, raw.get(), /*compressed_ok=*/false)); - ensure("nor a downscale", !r32ui->scaleDown(1)); + CHECK_FALSE(r32ui->readBackRaw(0, raw.get(), /*compressed_ok=*/false)); + CHECK_FALSE(r32ui->scaleDown(1)); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // The integer formats in the size and channel tables: the sized ones at their texels' bits, the three-channel ones // padded in VRAM as RGB8 is, and the pixel formats their data comes in at their channel counts. - template<> template<> - void llimagegl_object::test<27>() + TEST_CASE_METHOD(llimagegl_data, "LLImageGL: the size and channel tables cover the integer formats", "[LLImageGL]") { const std::pair bits[] = { @@ -1079,10 +1040,10 @@ namespace }; for (const auto& entry : bits) { - const std::string name = "format " + std::to_string(entry.first); - ensure_equals(name + " host bits", LLImageGL::dataFormatBits(entry.first), entry.second); + INFO("format " << entry.first); + REQUIRE(LLImageGL::dataFormatBits(entry.first) == entry.second); const S32 vram = entry.second == 24 ? 32 : entry.second == 48 ? 64 : entry.second == 96 ? 128 : entry.second; - ensure_equals(name + " VRAM bits", LLImageGL::dataFormatVRAMBits(entry.first), vram); + REQUIRE(LLImageGL::dataFormatVRAMBits(entry.first) == vram); } const std::pair components[] = @@ -1092,8 +1053,8 @@ namespace }; for (const auto& entry : components) { - ensure_equals("pixel format " + std::to_string(entry.first) + " components", - LLImageGL::dataFormatComponents(entry.first), entry.second); + INFO("pixel format " << entry.first); + REQUIRE(LLImageGL::dataFormatComponents(entry.first) == entry.second); } } } diff --git a/indra/llrender/tests/llrender2dutils_gl_test.cpp b/indra/llrender/tests/llrender2dutils_gl_test.cpp index 2132f31368..93baaf3531 100644 --- a/indra/llrender/tests/llrender2dutils_gl_test.cpp +++ b/indra/llrender/tests/llrender2dutils_gl_test.cpp @@ -33,15 +33,13 @@ #include "../llvertexbuffer.h" #include "llimage.h" -#include "../test/lltut.h" +#include #include #include #include #include -namespace tut -{ namespace { struct llrender2dutils_data @@ -121,66 +119,64 @@ namespace return b; } - static void ensureNear(const std::string& what, F32 actual, F32 expected) + static void require_near(const std::string& what, F32 actual, F32 expected) { - ensure(what + ": " + std::to_string(actual) + " vs " + std::to_string(expected), - fabsf(actual - expected) < 1e-3f); + INFO(what); + CAPTURE(actual, expected); + REQUIRE(fabsf(actual - expected) < 1e-3f); } }; - typedef test_group llrender2dutils_test; - typedef llrender2dutils_test::object llrender2dutils_object; - tut::llrender2dutils_test llrender2dutils_testcase("llrender2dutils_gl"); -} // A rotated image turns about its own centre and keeps its size. An // odd-sized image at a quarter turn covers the same pixels it does // unrotated -- an open folder arrow sits where the closed one did. - template<> template<> - void llrender2dutils_object::test<1>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: a rotated image turns about its own centre and keeps its size", "[llrender2dutils_gl]") { LLPointer texture = makeTexture(4, 4); const Bounds flat = boundsOf([&]() { gl_draw_scaled_rotated_image(10, 20, 9, 9, 0.f, texture.get()); }); - ensure_equals("unrotated, one quad", flat.count, (size_t)6); - ensureNear("unrotated left", flat.min_x, 10.f); - ensureNear("unrotated right", flat.max_x, 19.f); - ensureNear("unrotated bottom", flat.min_y, 20.f); - ensureNear("unrotated top", flat.max_y, 29.f); + UNSCOPED_INFO("unrotated, one quad"); + REQUIRE(flat.count == (size_t)6); + require_near("unrotated left", flat.min_x, 10.f); + require_near("unrotated right", flat.max_x, 19.f); + require_near("unrotated bottom", flat.min_y, 20.f); + require_near("unrotated top", flat.max_y, 29.f); const Bounds turned = boundsOf([&]() { gl_draw_scaled_rotated_image(10, 20, 9, 9, 90.f, texture.get()); }); - ensure_equals("turned, one quad", turned.count, (size_t)6); - ensureNear("turned left", turned.min_x, flat.min_x); - ensureNear("turned right", turned.max_x, flat.max_x); - ensureNear("turned bottom", turned.min_y, flat.min_y); - ensureNear("turned top", turned.max_y, flat.max_y); + UNSCOPED_INFO("turned, one quad"); + REQUIRE(turned.count == (size_t)6); + require_near("turned left", turned.min_x, flat.min_x); + require_near("turned right", turned.max_x, flat.max_x); + require_near("turned bottom", turned.min_y, flat.min_y); + require_near("turned top", turned.max_y, flat.max_y); } // The turn is a turn: at a quarter turn a wide image stands tall, and // it does so about the centre of the rect it was asked to fill. - template<> template<> - void llrender2dutils_object::test<2>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: a quarter turn stands a wide image tall about the rect's centre", "[llrender2dutils_gl]") { LLPointer texture = makeTexture(4, 4); const Bounds turned = boundsOf([&]() { gl_draw_scaled_rotated_image(10, 20, 8, 2, 90.f, texture.get()); }); - ensure_equals("one quad", turned.count, (size_t)6); + UNSCOPED_INFO("one quad"); + REQUIRE(turned.count == (size_t)6); // Centre (14, 21); half extents swap to (1, 4). - ensureNear("left", turned.min_x, 13.f); - ensureNear("right", turned.max_x, 15.f); - ensureNear("bottom", turned.min_y, 17.f); - ensureNear("top", turned.max_y, 25.f); + require_near("left", turned.min_x, 13.f); + require_near("right", turned.max_x, 15.f); + require_near("bottom", turned.min_y, 17.f); + require_near("top", turned.max_y, 25.f); } + // Rectangles added to one batch are one draw, where gl_rect_2d makes // a draw of each; and they are the same rectangles. - template<> template<> - void llrender2dutils_object::test<3>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: gl_rect_2d_in_batch draws the same rectangles as gl_rect_2d in one draw", "[llrender2dutils_gl]") { const auto count_draws = [](const std::function& draw) { std::list capture; @@ -210,9 +206,12 @@ namespace } gGL.end(); }); - ensure_equals("a draw apiece", apiece.first, (size_t)40); - ensure_equals("one draw in a batch", batched.first, (size_t)1); - ensure_equals("the same vertices", batched.second, apiece.second); + UNSCOPED_INFO("a draw apiece"); + CHECK(apiece.first == (size_t)40); + UNSCOPED_INFO("one draw in a batch"); + CHECK(batched.first == (size_t)1); + UNSCOPED_INFO("the same vertices"); + CHECK(batched.second == apiece.second); const Bounds b = boundsOf([] { gGL.getTextureSlot(0)->unbind(); @@ -221,18 +220,17 @@ namespace gl_rect_2d_in_batch(40, 60, 50, 55, LLColor4::white); gGL.end(); }); - ensureNear("left", b.min_x, 10.f); - ensureNear("right", b.max_x, 50.f); - ensureNear("bottom", b.min_y, 25.f); - ensureNear("top", b.max_y, 60.f); + require_near("left", b.min_x, 10.f); + require_near("right", b.max_x, 50.f); + require_near("bottom", b.min_y, 25.f); + require_near("top", b.max_y, 60.f); } // A rect's outline covers the pixels just inside its edges, and // nothing else: at the window's corner too, where a line along the // edge of a pixel landing outside the rect is not drawn at all; and // with the UI scaled up, a screen pixel thick, not a unit. - template<> template<> - void llrender2dutils_object::test<4>() + TEST_CASE_METHOD(llrender2dutils_data, "llrender2dutils_gl: a rect's outline lights only the pixels just inside its edges", "[llrender2dutils_gl]") { constexpr S32 W = ll_test::HeadlessGL::WIDTH; constexpr S32 H = ll_test::HeadlessGL::HEIGHT; @@ -264,8 +262,8 @@ namespace const U8 red = rgba[(static_cast(y) * W + x) * 4]; if ((red == 255) != edge) { - fail("at " + std::to_string(x) + "," + std::to_string(y) + " of the outline at " + std::to_string(rect.mLeft) + "," + - std::to_string(rect.mBottom) + ", scaled " + std::to_string(scale) + ": " + std::to_string(red)); + CAPTURE(rect.mLeft, rect.mBottom, scale, x, y, edge, static_cast(red)); + FAIL("a pixel off the outline is lit or one on it is not"); } } } diff --git a/indra/llrender/tests/llrender_test_checks.cpp b/indra/llrender/tests/llrender_test_checks.cpp new file mode 100644 index 0000000000..41a5e61cfb --- /dev/null +++ b/indra/llrender/tests/llrender_test_checks.cpp @@ -0,0 +1,158 @@ +/** + * @file llrender_test_checks.cpp + * @brief The state guard's checks on what llrender keeps for the process: the + * GL context, gGL's matrix stacks and switches, the bound shader, the + * live GL images, the font manager, and the DPI, scale and UI + * transform text is laid out at. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../llfontfreetype.h" +#include "../llfontgl.h" +#include "../llglslshader.h" +#include "../llimagegl.h" +#include "../llrender.h" + +#include "alteststateguard.h" + +#include "SDL3/SDL_video.h" + +#include +#include + +#include + +namespace ll_test +{ + // Friended in llrender.h, which keeps its stacks to itself. + struct RenderProbe + { + // How many matrices each of gGL's stacks has pushed, and which stack + // its matrix calls go to. + static std::string matrixStacks() + { + static const char* const names[LLRender::NUM_MATRIX_MODES] = { "modelview", "projection", "texture" }; + std::string text; + for (U32 mode = 0; mode < LLRender::NUM_MATRIX_MODES; ++mode) + { + text += std::string(names[mode]) + " " + std::to_string(gGL.mMatIdx[mode]) + ", "; + } + return text + "calls to " + names[gGL.mMatrixMode]; + } + }; +} + +namespace +{ + // The context current on this thread: its handle, or none. The hidden + // window's context is SDL's, on every platform. + std::string currentContext() + { + SDL_GLContext context = SDL_GL_GetCurrentContext(); + return context ? llformat("%p", static_cast(context)) : std::string("none"); + } + + // The errors GL holds for the next glGetError, which reading clears. + std::string unreadErrors() + { + if (!SDL_GL_GetCurrentContext()) + { + return "no context"; + } + std::string text; + // A context holds one flag per kind of error, so few can be pending. + for (S32 i = 0; i < 8; ++i) + { + const GLenum error = glGetError(); + if (error == GL_NO_ERROR) + { + break; + } + text += llformat("%s0x%04x", text.empty() ? "" : ", ", error); + } + return text.empty() ? std::string("none") : text; + } + + class LLRenderChecks : public Catch::EventListenerBase + { + public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) override + { + // A case that makes a context of its own ends it, and one on the + // shared context leaves that one current. + ll_test::StateGuard::add("the GL context", currentContext); + // A case pops what it pushed: the stacks are LL_MATRIX_STACK_DEPTH + // deep, and a case after one that left a push draws through it. + ll_test::StateGuard::add("gGL's matrix stacks", ll_test::RenderProbe::matrixStacks); + ll_test::StateGuard::add("the bound shader", [] + { + const LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; + return shader ? shader->mName : std::string("none"); + }); + ll_test::StateGuard::add("LLRender's switches", [] + { + return std::string("reverse-Z ") + (LLRender::sReverseZ ? "on" : "off") + + ", compute " + (LLRender::sCompute ? "on" : "off"); + }); + ll_test::StateGuard::add("the GL viewport", [] + { + return llformat("%d %d %d %d", gGLViewport[0], gGLViewport[1], gGLViewport[2], gGLViewport[3]); + }); + // A case reads the errors it made: left, the next case's own + // check for none would find them. Reading them clears them, so + // the guard reads what is left once it has reported, and the + // next case starts with none rather than being blamed for that. + ll_test::StateGuard::add("unread GL errors", unreadErrors, [] { unreadErrors(); }); + // A case that ends its context releases the images it made in it: + // one left over holds a name in a context that is gone. While a + // shared context lives, the images in it may outlive a case, as + // the font atlas pages it rasterizes into do. + ll_test::StateGuard::add("GL images outliving their context", [] + { + return SDL_GL_GetCurrentContext() ? std::string("a context is current") + : std::to_string(LLImageGL::sImageList.size()); + }); + ll_test::StateGuard::add("the font manager", + [] { return std::string(gFontManagerp ? "live" : "none"); }); + // The fonts are made for the process at one DPI and scale, which + // a later case's text is laid out and measured at. + ll_test::StateGuard::add("LLFontGL's DPI and scale", [] + { + return llformat("DPI %g x %g, scale %g x %g", LLFontGL::sHorizDPI, LLFontGL::sVertDPI, + LLFontGL::sScaleX, LLFontGL::sScaleY); + }); + // A case that moves the UI origin, or pushes a transform, puts it + // back: the next case's text and images land where it says. + ll_test::StateGuard::add("LLFontGL's UI transform", [] + { + return llformat("origin %g %g, depth %g, scale %g x %g, %zu pushed", LLFontGL::sCurOrigin.mX, + LLFontGL::sCurOrigin.mY, LLFontGL::sCurDepth, LLFontGL::sCurScaleX, + LLFontGL::sCurScaleY, LLFontGL::sOriginStack.size()); + }); + } + }; +} + +CATCH_REGISTER_LISTENER(LLRenderChecks) diff --git a/indra/llrender/tests/llrendermatrix_test.cpp b/indra/llrender/tests/llrendermatrix_test.cpp index 28c170bcaf..5d79013bcf 100644 --- a/indra/llrender/tests/llrendermatrix_test.cpp +++ b/indra/llrender/tests/llrendermatrix_test.cpp @@ -32,13 +32,11 @@ #include "m4math.h" #include "llquaternion.h" -#include "../test/lltut.h" +#include +#include #include -#include -namespace tut -{ namespace { struct llrendermatrix_data @@ -67,32 +65,28 @@ namespace } }; - typedef test_group llrendermatrix_test; - typedef llrendermatrix_test::object llrendermatrix_object; - tut::llrendermatrix_test llrendermatrix_testgroup("llrendermatrix"); - // Every element within tolerance of the larger magnitude, or of one. - void ensure_matrix_close(const std::string& what, const LLMatrix4a& actual, const LLMatrix4a& expected, F32 tolerance) + void require_matrix_close(const std::string& what, const LLMatrix4a& actual, const LLMatrix4a& expected, F32 tolerance) { + INFO(what); const F32* a = actual.getF32ptr(); const F32* e = expected.getF32ptr(); for (S32 i = 0; i < 16; ++i) { const F32 scale = llmax(1.f, fabsf(a[i]), fabsf(e[i])); - std::ostringstream msg; - msg << what << " at element " << i << ": " << a[i] << " vs " << e[i]; - ensure_approximately_equals_range(msg.str().c_str(), a[i], e[i], tolerance * scale); + CAPTURE(i); + REQUIRE_THAT(a[i], Catch::Matchers::WithinAbs(e[i], tolerance * scale)); } } - void ensure_vector_close(const std::string& what, const LLVector4a& actual, const LLVector4a& expected, F32 tolerance) + void require_vector_close(const std::string& what, const LLVector4a& actual, const LLVector4a& expected, F32 tolerance) { + INFO(what); for (S32 i = 0; i < 3; ++i) { const F32 scale = llmax(1.f, fabsf(actual[i]), fabsf(expected[i])); - std::ostringstream msg; - msg << what << " at lane " << i << ": " << actual[i] << " vs " << expected[i]; - ensure_approximately_equals_range(msg.str().c_str(), actual[i], expected[i], tolerance * scale); + CAPTURE(i); + REQUIRE_THAT(actual[i], Catch::Matchers::WithinAbs(expected[i], tolerance * scale)); } } @@ -117,13 +111,11 @@ namespace -0.8f, 0.6f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 5.f, -7.f, 9.f, 1.f }; -} // The stack ops apply the new transform ahead of what the stack held, // which on the scalar matrix is the product written the other way // round. - template<> template<> - void llrendermatrix_object::test<1>() + TEST_CASE_METHOD(llrendermatrix_data, "llrendermatrix: the stack ops apply the new transform ahead of what the stack held", "[llrendermatrix]") { gGL.loadMatrix(M_ROWS); gGL.translatef(1.f, 2.f, 3.f); @@ -146,52 +138,53 @@ namespace expected *= s; expected *= t; expected *= LLMatrix4(M_ROWS); - ensure_matrix_close("the stack after translate, scale, rotate, mult", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); + require_matrix_close("the stack after translate, scale, rotate, mult", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); // push copies, pop restores gGL.pushMatrix(); gGL.loadIdentity(); - ensure("loadIdentity", gGL.getModelviewMatrix() == LLMatrix4a::identity()); + CHECK(gGL.getModelviewMatrix() == LLMatrix4a::identity()); gGL.popMatrix(); - ensure_matrix_close("popMatrix restores", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); + require_matrix_close("popMatrix restores", gGL.getModelviewMatrix(), LLMatrix4a(expected), 1e-5f); // the modes are separate stacks gGL.matrixMode(LLRender::MM_PROJECTION); - ensure("the projection stack is untouched", gGL.getProjectionMatrix() == LLMatrix4a::identity()); + UNSCOPED_INFO("the projection stack is untouched"); + CHECK(gGL.getProjectionMatrix() == LLMatrix4a::identity()); } - // ortho on the stack and the projection helpers, in both depth conventions. - template<> template<> - void llrendermatrix_object::test<2>() + TEST_CASE_METHOD(llrendermatrix_data, "llrendermatrix: the projection helpers and gGL.ortho under forward and reverse-Z", "[llrendermatrix]") { for (bool reverse : { false, true }) { LLRender::sReverseZ = reverse; - const std::string when = reverse ? " under reverse-Z" : " forward"; + CAPTURE(reverse); const LLMatrix4a ortho = LLMatrix4a::ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f); const LLMatrix4a persp = LLMatrix4a::perspective(1.1f, 1.777f, 0.25f, 1024.f); const LLMatrix4a expected_ortho = reverse ? reversed_z(ortho) : ortho; const LLMatrix4a expected_persp = reverse ? reversed_z(persp) : persp; - ensure("al_ortho" + when, same_bytes(al_ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), expected_ortho)); - ensure("al_perspective" + when, same_bytes(al_perspective(1.1f, 1.777f, 0.25f, 1024.f), expected_persp)); - ensure("al_reverse_z_transform" + when, same_bytes(al_reverse_z_transform(persp), reversed_z(persp))); + REQUIRE(same_bytes(al_ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), expected_ortho)); + REQUIRE(same_bytes(al_perspective(1.1f, 1.777f, 0.25f, 1024.f), expected_persp)); + REQUIRE(same_bytes(al_reverse_z_transform(persp), reversed_z(persp))); // a reversed projection puts the near plane at window 1 and the // far at 0 LLVector4a near_clip, far_clip; expected_persp.transform4(LLVector4a(0.f, 0.f, -0.25f, 1.f), near_clip); expected_persp.transform4(LLVector4a(0.f, 0.f, -1024.f, 1.f), far_clip); - ensure_approximately_equals_range(("near plane depth" + when).c_str(), near_clip[2] / near_clip[3], reverse ? 1.f : -1.f, 1e-5f); - ensure_approximately_equals_range(("far plane depth" + when).c_str(), far_clip[2] / far_clip[3], reverse ? 0.f : 1.f, 1e-5f); + UNSCOPED_INFO("near plane depth"); + REQUIRE_THAT(near_clip[2] / near_clip[3], Catch::Matchers::WithinAbs(reverse ? 1.f : -1.f, 1e-5f)); + UNSCOPED_INFO("far plane depth"); + REQUIRE_THAT(far_clip[2] / far_clip[3], Catch::Matchers::WithinAbs(reverse ? 0.f : 1.f, 1e-5f)); gGL.matrixMode(LLRender::MM_PROJECTION); gGL.loadMatrix(M_ROWS); gGL.ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f); LLMatrix4a expected_stack; expected_stack.setMul(expected_ortho, LLMatrix4a(M_ROWS)); - ensure_matrix_close("gGL.ortho applies ahead of the stack" + when, gGL.getProjectionMatrix(), expected_stack, 1e-5f); + require_matrix_close("gGL.ortho applies ahead of the stack", gGL.getProjectionMatrix(), expected_stack, 1e-5f); gGL.loadIdentity(); gGL.matrixMode(LLRender::MM_MODELVIEW); @@ -202,24 +195,24 @@ namespace const LLVector4a win = al_project(obj, mv, expected_persp, viewport); const LLVector4a expected_win = reverse ? alprojection::project_zo(obj, mv, expected_persp, viewport) : alprojection::project(obj, mv, expected_persp, viewport); - ensure_vector_close("al_project" + when, win, expected_win, 1e-6f); - ensure("window depth is in [0, 1]" + when, win[2] >= 0.f && win[2] <= 1.f); + require_vector_close("al_project", win, expected_win, 1e-6f); + REQUIRE(win[2] >= 0.f); + REQUIRE(win[2] <= 1.f); const LLVector4a back = al_unproject(win, mv, expected_persp, viewport); - ensure_vector_close("al_unproject of al_project" + when, back, obj, 5e-4f); + require_vector_close("al_unproject of al_project", back, obj, 5e-4f); LLMatrix4a inverse; inverse.setMul(mv, expected_persp); inverse.invert(); - ensure_vector_close("al_unproject through the inverse" + when, al_unproject(win, inverse, viewport), back, 1e-6f); + require_vector_close("al_unproject through the inverse", al_unproject(win, inverse, viewport), back, 1e-6f); } } // The oblique clip plane: the plane through the inverse transpose of // the modelview then projection, normalized on depth, applied after // the projection as its depth. - template<> template<> - void llrendermatrix_object::test<4>() + TEST_CASE_METHOD(llrendermatrix_data, "llrendermatrix: LLGLUserClipPlane rewrites only the depth column and puts it back", "[llrendermatrix]") { const LLMatrix4a persp = LLMatrix4a::perspective(1.1f, 1.777f, 0.25f, 1024.f); LLMatrix4a mv; @@ -250,17 +243,20 @@ namespace suffix.setColumn<2>(cplane); LLMatrix4a expected; expected.setMul(persp, suffix); - ensure_matrix_close("oblique projection", gGL.getProjectionMatrix(), expected, 1e-5f); + require_matrix_close("oblique projection", gGL.getProjectionMatrix(), expected, 1e-5f); // only the depth column moved for (S32 column : { 0, 1, 3 }) { for (S32 row = 0; row < 4; ++row) { - ensure_equals("the other columns are the projection's", gGL.getProjectionMatrix().mMatrix[row][column], persp.mMatrix[row][column]); + CAPTURE(column, row); + UNSCOPED_INFO("the other columns are the projection's"); + REQUIRE(gGL.getProjectionMatrix().mMatrix[row][column] == persp.mMatrix[row][column]); } } } - ensure("projection restored", same_bytes(gGL.getProjectionMatrix(), persp)); + UNSCOPED_INFO("projection restored"); + CHECK(same_bytes(gGL.getProjectionMatrix(), persp)); } } diff --git a/indra/llrender/tests/llrendertarget_gl_test.cpp b/indra/llrender/tests/llrendertarget_gl_test.cpp index 6f3126b69f..7190efe840 100644 --- a/indra/llrender/tests/llrendertarget_gl_test.cpp +++ b/indra/llrender/tests/llrendertarget_gl_test.cpp @@ -32,16 +32,14 @@ #include "llheadlessgl_fixture.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { - namespace - { constexpr U32 SIZE = 4; // A triangle over the viewport. @@ -102,12 +100,13 @@ namespace tut // Every texel of `texels` is `expected`. template - void ensureEvery(const std::string& what, const std::vector& texels, const T (&expected)[4]) + void require_every(const std::string& what, const std::vector& texels, const T (&expected)[4]) { + INFO(what); for (size_t i = 0; i < texels.size(); ++i) { - ensure_equals(what + ", texel " + std::to_string(i / 4) + " channel " + std::to_string(i % 4), texels[i], - expected[i % 4]); + INFO("texel " << i / 4 << " channel " << i % 4); + REQUIRE(texels[i] == expected[i % 4]); } } @@ -143,11 +142,15 @@ namespace tut } // The integer program, linked. - GLuint linkInteger() + GLuint require_integer_program() { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kIntegerFragment); - ensure("integer shaders compile", vs && fs); + { + INFO("integer shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } const GLuint program = glCreateProgram(); glAttachShader(program, vs); glAttachShader(program, fs); @@ -156,7 +159,7 @@ namespace tut glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(program, GL_LINK_STATUS, &linked); - ensure("integer program links", linked == GL_TRUE); + REQUIRE(linked == GL_TRUE); return program; } @@ -164,7 +167,11 @@ namespace tut { const GLuint vs = ll_test::compileTestShader(GL_VERTEX_SHADER, kVertex); const GLuint fs = ll_test::compileTestShader(GL_FRAGMENT_SHADER, kFragment); - ensure("shaders compile", vs && fs); + { + INFO("shaders compile"); + REQUIRE(vs); + REQUIRE(fs); + } mProgram = glCreateProgram(); glAttachShader(mProgram, vs); glAttachShader(mProgram, fs); @@ -173,10 +180,10 @@ namespace tut glDeleteShader(fs); GLint linked = GL_FALSE; glGetProgramiv(mProgram, GL_LINK_STATUS, &linked); - ensure("program links", linked == GL_TRUE); + REQUIRE(linked == GL_TRUE); glGenVertexArrays(1, &mVAO); - ensure("target allocates", mTarget.allocate(SIZE, SIZE, GL_RGBA8)); + REQUIRE(mTarget.allocate(SIZE, SIZE, GL_RGBA8)); } // What the target holds after the triangle draws into it, its draw buffers set to output `output`, or left as @@ -214,66 +221,65 @@ namespace tut GLuint mIntegerProgram = 0; }; - typedef test_group llrendertarget_t; - typedef llrendertarget_t::object llrendertarget_object_t; - tut::llrendertarget_t tut_llrendertarget("llrendertarget_gl"); - } // setDrawOutput: a single-attachment target takes the output it names and none of the others, and the next // bindTarget gives it output 0 again. - template<> template<> - void llrendertarget_object_t::test<1>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: setDrawOutput sends the named output to a single-attachment target", "[llrendertarget_gl]") { setUp(); - ensure_equals("bound: output 0", drawn(-1), OUTPUT_COLOURS[0]); + UNSCOPED_INFO("bound: output 0"); + CHECK(drawn(-1) == OUTPUT_COLOURS[0]); for (S32 output = 0; output < 4; ++output) { - ensure_equals("output " + std::to_string(output), drawn(output), OUTPUT_COLOURS[output]); + CAPTURE(output); + REQUIRE(drawn(output) == OUTPUT_COLOURS[output]); } - ensure_equals("rebound: output 0 again", drawn(-1), OUTPUT_COLOURS[0]); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + UNSCOPED_INFO("rebound: output 0 again"); + CHECK(drawn(-1) == OUTPUT_COLOURS[0]); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // Integer attachments: R32UI, RG16I and RGBA32UI allocate as themselves beside a depth buffer. clear takes each to // the clear colour rounded to integers -- an unsigned one held at 0 below and at its top above -- and leaves them // alone when only the depth is asked for; a program's integer outputs reach them exactly. - template<> template<> - void llrendertarget_object_t::test<2>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: integer attachments keep their formats through clear and draw", "[llrendertarget_gl]") { - mIntegerProgram = linkInteger(); + mIntegerProgram = require_integer_program(); glGenVertexArrays(1, &mVAO); - ensure("R32UI target allocates", mInteger.allocate(INT_W, INT_H, GL_R32UI, /*depth=*/true)); - ensure("RG16I attaches", mInteger.addColorAttachment(GL_RG16I)); - ensure("RGBA32UI attaches", mInteger.addColorAttachment(GL_RGBA32UI)); + REQUIRE(mInteger.allocate(INT_W, INT_H, GL_R32UI, /*depth=*/true)); + CHECK(mInteger.addColorAttachment(GL_RG16I)); + CHECK(mInteger.addColorAttachment(GL_RGBA32UI)); const GLenum formats[] = { GL_R32UI, GL_RG16I, GL_RGBA32UI }; for (U32 attachment = 0; attachment < 3; ++attachment) { - ensure_equals("attachment " + std::to_string(attachment) + " recorded", mInteger.getInternalFormat(attachment), - (U32)formats[attachment]); + CAPTURE(attachment); + UNSCOPED_INFO("the target's record"); + REQUIRE(mInteger.getInternalFormat(attachment) == (U32)formats[attachment]); gGL.getTextureSlot(0)->bindManual(ALTextureSlot::TT_TEXTURE, mInteger.getTexture(attachment)); GLint format = 0; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); - ensure_equals("attachment " + std::to_string(attachment) + " storage", (U32)format, (U32)formats[attachment]); + UNSCOPED_INFO("the texture's storage"); + REQUIRE((U32)format == (U32)formats[attachment]); } gGL.getTextureSlot(0)->unbind(); mInteger.bindTarget(); - ensure_equals("complete", (U32)glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER), (U32)GL_FRAMEBUFFER_COMPLETE); + CHECK((U32)glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == (U32)GL_FRAMEBUFFER_COMPLETE); gGL.setClearColor(LLColor4(7.f, 9.f, 11.f, 13.f)); mInteger.clear(); - ensureEvery("R32UI cleared", readInteger(0), { 7u, 0u, 0u, 1u }); - ensureEvery("RG16I cleared", readInteger(1), { 7, 9, 0, 1 }); - ensureEvery("RGBA32UI cleared", readInteger(2), { 7u, 9u, 11u, 13u }); + require_every("R32UI cleared", readInteger(0), { 7u, 0u, 0u, 1u }); + require_every("RG16I cleared", readInteger(1), { 7, 9, 0, 1 }); + require_every("RGBA32UI cleared", readInteger(2), { 7u, 9u, 11u, 13u }); gGL.setClearColor(LLColor4(-5.4f, -6.6f, 1.e10f, 0.4f)); mInteger.clear(GL_DEPTH_BUFFER_BIT); - ensureEvery("R32UI left alone by a depth clear", readInteger(0), { 7u, 0u, 0u, 1u }); + require_every("R32UI left alone by a depth clear", readInteger(0), { 7u, 0u, 0u, 1u }); mInteger.clear(); - ensureEvery("R32UI held at 0", readInteger(0), { 0u, 0u, 0u, 1u }); - ensureEvery("RG16I below 0", readInteger(1), { -5, -7, 0, 1 }); - ensureEvery("RGBA32UI held at its top", readInteger(2), { 0u, 0u, 0xFFFFFFFFu, 0u }); + require_every("R32UI held at 0", readInteger(0), { 0u, 0u, 0u, 1u }); + require_every("RG16I below 0", readInteger(1), { -5, -7, 0, 1 }); + require_every("RGBA32UI held at its top", readInteger(2), { 0u, 0u, 0xFFFFFFFFu, 0u }); LLGLDisable depth_test(GL_DEPTH_TEST); glUseProgram(mIntegerProgram); @@ -290,27 +296,33 @@ namespace tut { for (U32 x = 0; x < INT_W; ++x) { - const std::string at = " at " + std::to_string(x) + "," + std::to_string(y); + CAPTURE(x, y); const size_t i = ((size_t)y * INT_W + x) * 4; - ensure_equals("R32UI drawn" + at, uints[i], 0x01000001u + y * 1000u + x); - ensure_equals("RG16I r drawn" + at, ints[i], -1 - (S32)x); - ensure_equals("RG16I g drawn" + at, ints[i + 1], (S32)y * 300 - 1000); - ensure_equals("RGBA32UI r drawn" + at, wide[i], x); - ensure_equals("RGBA32UI g drawn" + at, wide[i + 1], y); - ensure_equals("RGBA32UI b drawn" + at, wide[i + 2], 0xFFFFFFFFu - x); - ensure_equals("RGBA32UI a drawn" + at, wide[i + 3], 0x80000000u + y); + UNSCOPED_INFO("R32UI drawn"); + REQUIRE(uints[i] == 0x01000001u + y * 1000u + x); + UNSCOPED_INFO("RG16I r drawn"); + REQUIRE(ints[i] == -1 - (S32)x); + UNSCOPED_INFO("RG16I g drawn"); + REQUIRE(ints[i + 1] == (S32)y * 300 - 1000); + UNSCOPED_INFO("RGBA32UI r drawn"); + REQUIRE(wide[i] == x); + UNSCOPED_INFO("RGBA32UI g drawn"); + REQUIRE(wide[i + 1] == y); + UNSCOPED_INFO("RGBA32UI b drawn"); + REQUIRE(wide[i + 2] == 0xFFFFFFFFu - x); + UNSCOPED_INFO("RGBA32UI a drawn"); + REQUIRE(wide[i + 3] == 0x80000000u + y); } } - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // A target of both kinds: clear takes the float attachment to the clear colour and the integer one to its integers, // and with the draw buffers narrowed to the first it clears that one alone, as it would a target of one kind. - template<> template<> - void llrendertarget_object_t::test<3>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: a mixed float and integer target clears each attachment as its kind", "[llrendertarget_gl]") { - ensure("RGBA8 target allocates", mMixed.allocate(INT_W, INT_H, GL_RGBA8)); - ensure("R32UI attaches", mMixed.addColorAttachment(GL_R32UI)); + REQUIRE(mMixed.allocate(INT_W, INT_H, GL_RGBA8)); + CHECK(mMixed.addColorAttachment(GL_R32UI)); mMixed.bindTarget(); gGL.setClearColor(LLColor4(1.f, 0.f, 1.f, 1.f)); @@ -318,42 +330,41 @@ namespace tut std::vector rgba((size_t)INT_W * INT_H * 4, 0x55); glReadBuffer(GL_COLOR_ATTACHMENT0); glReadPixels(0, 0, INT_W, INT_H, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); - ensureEvery("RGBA8 cleared", rgba, { 255, 0, 255, 255 }); - ensureEvery("R32UI cleared", readInteger(1), { 1u, 0u, 0u, 1u }); + require_every("RGBA8 cleared", rgba, { 255, 0, 255, 255 }); + require_every("R32UI cleared", readInteger(1), { 1u, 0u, 0u, 1u }); mMixed.setDrawBuffers(1); gGL.setClearColor(LLColor4::transparent); mMixed.clear(); glReadPixels(0, 0, INT_W, INT_H, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); - ensureEvery("RGBA8 cleared again", rgba, { 0, 0, 0, 0 }); - ensureEvery("R32UI not drawn to, so left", readInteger(1), { 1u, 0u, 0u, 1u }); + require_every("RGBA8 cleared again", rgba, { 0, 0, 0, 0 }); + require_every("R32UI not drawn to, so left", readInteger(1), { 1u, 0u, 0u, 1u }); mMixed.flush(); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } // A texture lent by setColorAttachment answers for its format as one the target allocated does, so an integer one // clears to its integers; and returning it forgets the format. - template<> template<> - void llrendertarget_object_t::test<4>() + TEST_CASE_METHOD(llrendertarget_data, "llrendertarget_gl: a texture lent by setColorAttachment has its format until returned", "[llrendertarget_gl]") { const std::vector zeros((size_t)INT_W * INT_H, 0u); mLentImage = new LLImageGL(INT_W, INT_H, 1, /*usemipmaps=*/false); mLentImage->setExplicitFormat(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT); - ensure("R32UI image created", mLentImage->createGLTexture(0, reinterpret_cast(zeros.data()))); + CHECK(mLentImage->createGLTexture(0, reinterpret_cast(zeros.data()))); LLRenderTarget::sUseFBO = true; mLent.setColorAttachment(mLentImage.get()); - ensure_equals("the lent texture's format", mLent.getInternalFormat(0), (U32)GL_R32UI); + CHECK(mLent.getInternalFormat(0) == (U32)GL_R32UI); mLent.bindTarget(); gGL.setClearColor(LLColor4(5.f, 6.f, 7.f, 8.f)); mLent.clear(); - ensureEvery("R32UI cleared to its integers", readInteger(0), { 5u, 0u, 0u, 1u }); + require_every("R32UI cleared to its integers", readInteger(0), { 5u, 0u, 0u, 1u }); mLent.flush(); mLent.releaseColorAttachment(); - ensure_equals("returned, its format forgotten", mLent.getInternalFormat(0), 0u); - ensure_equals("no GL error", (U32)glGetError(), (U32)GL_NO_ERROR); + CHECK(mLent.getInternalFormat(0) == 0u); + CHECK((U32)glGetError() == (U32)GL_NO_ERROR); } } diff --git a/indra/llrender/tests/lluiimage_test.cpp b/indra/llrender/tests/lluiimage_test.cpp index d9d0ef772e..bc6e8210d7 100644 --- a/indra/llrender/tests/lluiimage_test.cpp +++ b/indra/llrender/tests/lluiimage_test.cpp @@ -39,7 +39,7 @@ #include "../llvertexbuffer.h" #include "llimage.h" -#include "../test/lltut.h" +#include #include #include @@ -88,8 +88,6 @@ namespace ll_test }; } -namespace tut -{ namespace { struct lluiimage_data @@ -123,9 +121,9 @@ namespace } // A recording's bounds are the image's size, from the origin. - static void ensureExtent(const std::vector& verts, F32 width, F32 height) + static void require_extent(const std::vector& verts, F32 width, F32 height) { - ensure("the recording holds vertices", !verts.empty()); + REQUIRE_FALSE(verts.empty()); F32 min_x = verts[0].mV[VX]; F32 max_x = min_x; F32 min_y = verts[0].mV[VY]; @@ -137,55 +135,49 @@ namespace min_y = llmin(min_y, v.mV[VY]); max_y = llmax(max_y, v.mV[VY]); } - ensure("starts at the origin in x", min_x == 0.f); - ensure("starts at the origin in y", min_y == 0.f); - ensure("spans the width", max_x == width); - ensure("spans the height", max_y == height); + REQUIRE(min_x == 0.f); + REQUIRE(min_y == 0.f); + REQUIRE(max_x == width); + REQUIRE(max_y == height); } }; - typedef test_group lluiimage_test; - typedef lluiimage_test::object lluiimage_object; - tut::lluiimage_test lluiimage_testcase("LLUIImage"); -} // One recording serves every position the image is drawn at. A floater // being dragged is this test run at frame rate. - template<> template<> - void lluiimage_object::test<1>() + TEST_CASE_METHOD(lluiimage_data, "LLUIImage: one recording serves every position but not every size or colour", "[LLUIImage]") { LLPointer texture = makeTexture(4, 4); LLPointer image = new LLUIImage("probe", texture.get()); image->draw(10, 10, 8, 8); - ensure_equals("the first draw records", - ll_test::UIImageProbe::recordings(*image), (size_t)1); + UNSCOPED_INFO("the first draw records"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)1); image->draw(100, 50, 8, 8); - ensure_equals("another position replays", - ll_test::UIImageProbe::recordings(*image), (size_t)1); + UNSCOPED_INFO("another position replays"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)1); gGL.pushUIMatrix(); gGL.translateUI(30.f, 40.f, 0.f); image->draw(10, 10, 8, 8); gGL.popUIMatrix(); - ensure_equals("another UI offset replays", - ll_test::UIImageProbe::recordings(*image), (size_t)1); + UNSCOPED_INFO("another UI offset replays"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)1); image->draw(10, 10, 16, 8); - ensure_equals("another size is another recording", - ll_test::UIImageProbe::recordings(*image), (size_t)2); + UNSCOPED_INFO("another size is another recording"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)2); image->draw(10, 10, 8, 8, LLColor4::red); - ensure_equals("another colour is another recording", - ll_test::UIImageProbe::recordings(*image), (size_t)3); + UNSCOPED_INFO("another colour is another recording"); + CHECK(ll_test::UIImageProbe::recordings(*image) == (size_t)3); } // The recording holds the image's shape and nothing of where it was made: // drawn under a UI offset and away from the origin, its vertices still // span exactly [0, width] x [0, height]. - template<> template<> - void lluiimage_object::test<2>() + TEST_CASE_METHOD(lluiimage_data, "LLUIImage: a plain image's recording spans its size from the origin", "[LLUIImage]") { LLPointer texture = makeTexture(4, 4); LLPointer image = new LLUIImage("probe", texture.get()); @@ -196,14 +188,14 @@ namespace gGL.popUIMatrix(); const std::vector verts = ll_test::UIImageProbe::positions(*image); - ensure_equals("a plain image is one quad", verts.size(), (size_t)6); - ensureExtent(verts, 8.f, 6.f); + UNSCOPED_INFO("a plain image is one quad"); + REQUIRE(verts.size() == (size_t)6); + require_extent(verts, 8.f, 6.f); } // The nine-slice path works out its own placement from the UI offset, so // it is checked apart from the plain quad. - template<> template<> - void lluiimage_object::test<3>() + TEST_CASE_METHOD(lluiimage_data, "LLUIImage: a nine-slice image's recording spans its size from the origin", "[LLUIImage]") { LLPointer texture = makeTexture(16, 16); LLPointer image = new LLUIImage("probe", texture.get()); @@ -215,7 +207,8 @@ namespace gGL.popUIMatrix(); const std::vector verts = ll_test::UIImageProbe::positions(*image); - ensure_equals("nine slices are eighteen triangles", verts.size(), (size_t)54); - ensureExtent(verts, 32.f, 24.f); + UNSCOPED_INFO("nine slices are eighteen triangles"); + REQUIRE(verts.size() == (size_t)54); + require_extent(verts, 32.f, 24.f); } } diff --git a/indra/llrender/tests/lluitransform_test.cpp b/indra/llrender/tests/lluitransform_test.cpp index 7e0af297f1..56c4604d77 100644 --- a/indra/llrender/tests/lluitransform_test.cpp +++ b/indra/llrender/tests/lluitransform_test.cpp @@ -27,10 +27,8 @@ #include "../llfontgl.h" #include "../llrender2dutils.h" -#include "../test/lltut.h" +#include -namespace tut -{ namespace { struct lluitransform_data @@ -61,30 +59,23 @@ namespace } }; - typedef test_group lluitransform_test; - typedef lluitransform_test::object lluitransform_object; - tut::lluitransform_test lluitransform_testgroup("lluitransform"); -} - // With nothing pushed, a local rect is where it says it is. - template<> template<> - void lluitransform_object::test<1>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: with nothing pushed a local rect is where it says it is", "[lluitransform]") { const LLRect local(10, 40, 110, 20); const LLRect screen = LLRender2D::toScreen(local); - ensure_equals("untransformed, it is itself " + where(screen), where(screen), where(local)); + CHECK(where(screen) == where(local)); } // An offset moves it, which is all this ever did. - template<> template<> - void lluitransform_object::test<2>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: an offset moves a rect and keeps its size", "[lluitransform]") { LLFontGL::sCurOrigin.set(7, 13); const LLRect screen = LLRender2D::toScreen(LLRect(10, 40, 110, 20)); - ensure_equals("moved by the offset", screen.mLeft, 17); - ensure_equals("in both directions", screen.mBottom, 33); - ensure_equals("keeping its width", screen.getWidth(), 100); - ensure_equals("and its height", screen.getHeight(), 20); + CHECK(screen.mLeft == 17); + CHECK(screen.mBottom == 33); + CHECK(screen.getWidth() == 100); + CHECK(screen.getHeight() == 20); } // A scale grows it about the screen origin, and the offset is scaled with @@ -92,36 +83,41 @@ namespace // arithmetic the renderer does to every one of them. Clipping used to // apply the offset and not the scale, so a scissor sat where the drawing // would have been at a hundred per cent. - template<> template<> - void lluitransform_object::test<3>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: a scale grows a rect about the screen origin and scales the offset", "[lluitransform]") { LLFontGL::sCurOrigin.set(10, 20); LLFontGL::sCurScaleX = 2.f; LLFontGL::sCurScaleY = 2.f; const LLRect screen = LLRender2D::toScreen(LLRect(0, 50, 100, 0)); - ensure_equals("the near edge is the scaled offset", screen.mLeft, 20); - ensure_equals("and so is the bottom", screen.mBottom, 40); - ensure_equals("the far edge is scaled too", screen.mRight, 220); - ensure_equals("the rect is twice the size", screen.getWidth(), 200); - ensure_equals("in both directions", screen.getHeight(), 100); + { + INFO("the near edges are the scaled offset"); + CHECK(screen.mLeft == 20); + CHECK(screen.mBottom == 40); + } + UNSCOPED_INFO("the far edge is scaled too"); + CHECK(screen.mRight == 220); + { + INFO("the rect is twice the size"); + CHECK(screen.getWidth() == 200); + CHECK(screen.getHeight() == 100); + } } // The two axes are independent, because the renderer's are. - template<> template<> - void lluitransform_object::test<4>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: the two axes scale independently", "[lluitransform]") { LLFontGL::sCurScaleX = 3.f; LLFontGL::sCurScaleY = 1.f; const LLRect screen = LLRender2D::toScreen(LLRect(0, 10, 10, 0)); - ensure_equals("across", screen.getWidth(), 30); - ensure_equals("down is left alone", screen.getHeight(), 10); + CHECK(screen.getWidth() == 30); + UNSCOPED_INFO("down is left alone"); + CHECK(screen.getHeight() == 10); } // A point and a rect agree, since the rect is two points. - template<> template<> - void lluitransform_object::test<5>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: a point and a rect's corner land in the same place", "[lluitransform]") { LLFontGL::sCurOrigin.set(4, 6); LLFontGL::sCurScaleX = 1.5f; @@ -130,19 +126,21 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(20.f, 30.f, x, y); - ensure_equals("across", (S32)ll_round(x), 36); - ensure_equals("down", (S32)ll_round(y), 90); + CHECK((S32)ll_round(x) == 36); + CHECK((S32)ll_round(y) == 90); const LLRect screen = LLRender2D::toScreen(LLRect(20, 30, 20, 30)); - ensure_equals("the rect's corner is the same point", screen.mLeft, 36); - ensure_equals("and the same on the other axis", screen.mBottom, 90); + { + INFO("the rect's corner is the same point"); + CHECK(screen.mLeft == 36); + CHECK(screen.mBottom == 90); + } } // A scale keeps the origin it was pushed at: what is under it is drawn // larger where it already was, rather than being carried away from the // window's corner in proportion to how far along it had got. - template<> template<> - void lluitransform_object::test<6>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: a scale keeps the origin it was pushed at", "[lluitransform]") { LLRender2D::pushMatrix(); LLRender2D::translate(300.f, 200.f); @@ -151,20 +149,25 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(0.f, 0.f, x, y); - ensure_equals("the corner it was pushed at is where it was", ll_round(x), 300); - ensure_equals("on both axes", ll_round(y), 200); + { + INFO("the corner it was pushed at is where it was"); + CHECK(ll_round(x) == 300); + CHECK(ll_round(y) == 200); + } LLRender2D::toScreen(50.f, 40.f, x, y); - ensure_equals("and what is on it is twice as far along", ll_round(x), 400); - ensure_equals("in both directions", ll_round(y), 280); + { + INFO("what is on it is twice as far along"); + CHECK(ll_round(x) == 400); + CHECK(ll_round(y) == 280); + } LLRender2D::popMatrix(); } // Taking the frame off takes the scale with it, and what it did to the // offset to keep the origin. - template<> template<> - void lluitransform_object::test<7>() + TEST_CASE_METHOD(lluitransform_data, "lluitransform: popMatrix takes off the frame's scale and what it did to the offset", "[lluitransform]") { LLRender2D::pushMatrix(); LLRender2D::translate(30.f, 40.f); @@ -175,9 +178,13 @@ namespace F32 x = 0.f; F32 y = 0.f; LLRender2D::toScreen(10.f, 10.f, x, y); - ensure_equals("the offset is the one the frame was pushed on", ll_round(x), 40); - ensure_equals("on both axes", ll_round(y), 50); - ensure_equals("and the scale is off with it", LLFontGL::sCurScaleX, 1.f); + { + INFO("the offset is the one the frame was pushed on"); + CHECK(ll_round(x) == 40); + CHECK(ll_round(y) == 50); + } + UNSCOPED_INFO("the scale is off with it"); + CHECK(LLFontGL::sCurScaleX == 1.f); LLRender2D::popMatrix(); }