#include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include #include "engine/load_file.hpp" #include "engine/palette.h" #include "engine/point.hpp" #include "engine/rectangle.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "engine/size.hpp" #include "engine/surface.hpp" #include "utils/paths.h" #include "utils/sdl_compat.h" #include "utils/sdl_wrap.h" #include "utils/str_cat.hpp" #include "utils/surface_to_png.hpp" // Invoke with --update_expected to update the expected files with actual results. static bool UpdateExpected; namespace devilution { namespace { constexpr char FixturesPath[] = "test/fixtures/text_render_integration_test/"; struct TestFixture { std::string name; int width; int height; std::string_view fmt; std::vector args {}; TextRenderOptions opts { .flags = UiFlags::ColorUiGold }; friend void PrintTo(const TestFixture &f, std::ostream *os) { *os << f.name; } }; const TestFixture Fixtures[] { TestFixture { .name = "basic", .width = 96, .height = 15, .fmt = "DrawString", }, TestFixture { .name = "basic-colors", .width = 186, .height = 15, .fmt = "{}{}{}{}", .args = { { "Draw", UiFlags::ColorUiSilver }, { "String", UiFlags::ColorUiGold }, { "With", UiFlags::ColorUiSilverDark }, { "Colors", UiFlags::ColorUiGoldDark }, }, }, TestFixture { .name = "horizontal_overflow", .width = 50, .height = 28, .fmt = "Horizontal", }, TestFixture { .name = "horizontal_overflow-colors", .width = 50, .height = 28, .fmt = "{}{}", .args = { { "Hori", UiFlags::ColorUiGold }, { "zontal", UiFlags::ColorUiSilverDark }, }, }, TestFixture { .name = "kerning_fit_spacing", .width = 120, .height = 15, .fmt = "KerningFitSpacing", .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::ColorUiSilver, }, }, TestFixture { .name = "kerning_fit_spacing-colors", .width = 120, .height = 15, .fmt = "{}{}{}", .args = { { "Kerning", UiFlags::ColorUiSilver }, { "Fit", UiFlags::ColorUiGold }, { "Spacing", UiFlags::ColorUiSilverDark }, }, .opts = { .flags = UiFlags::KerningFitSpacing, }, }, TestFixture { .name = "kerning_fit_spacing__align_center", .width = 170, .height = 15, .fmt = "KerningFitSpacing | AlignCenter", .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::ColorUiSilver, }, }, TestFixture { .name = "kerning_fit_spacing__align_center-colors", .width = 170, .height = 15, .fmt = "{}{}{}", .args = { { "KerningFitSpacing", UiFlags::ColorUiSilver }, { " | ", UiFlags::ColorUiGold }, { "AlignCenter", UiFlags::ColorUiSilverDark }, }, .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter, }, }, TestFixture { .name = "kerning_fit_spacing__align_center__newlines", .width = 170, .height = 42, .fmt = "KerningFitSpacing | AlignCenter\nShort line\nAnother overly long line", .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::ColorUiSilver, }, }, TestFixture { .name = "kerning_fit_spacing__align_center__newlines_in_fmt-colors", .width = 170, .height = 42, .fmt = "{}\n{}\n{}", .args = { { "KerningFitSpacing | AlignCenter", UiFlags::ColorUiSilver }, { "Short line", UiFlags::ColorUiGold }, { "Another overly long line", UiFlags::ColorUiSilverDark }, }, .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter, }, }, TestFixture { .name = "kerning_fit_spacing__align_center__newlines_in_value-colors", .width = 170, .height = 42, .fmt = "{}{}", .args = { { "KerningFitSpacing | AlignCenter\nShort line\nAnother overly ", UiFlags::ColorUiSilver }, { "long line", UiFlags::ColorUiGold }, }, .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter, }, }, TestFixture { .name = "kerning_fit_spacing__align_right", .width = 170, .height = 15, .fmt = "KerningFitSpacing | AlignRight", .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignRight | UiFlags::ColorUiSilver, }, }, TestFixture { .name = "kerning_fit_spacing__align_right-colors", .width = 170, .height = 15, .fmt = "{}{}{}", .args = { { "KerningFitSpacing", UiFlags::ColorUiSilver }, { " | ", UiFlags::ColorUiGold }, { "AlignRight", UiFlags::ColorUiSilverDark }, }, .opts = { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignRight, }, }, TestFixture { .name = "vertical_overflow", .width = 36, .height = 20, .fmt = "One\nTwo", }, TestFixture { .name = "vertical_overflow-colors", .width = 36, .height = 20, .fmt = "{}\n{}", .args = { { "One", UiFlags::ColorUiGold }, { "Two", UiFlags::ColorUiSilverDark }, }, }, }; SDLPaletteUniquePtr LoadPalette() { struct Color { uint8_t r, g, b; }; std::array palData; LoadFileInMem("ui_art\\diablo.pal", palData); SDLPaletteUniquePtr palette = SDLWrap::AllocPalette(static_cast(palData.size())); for (unsigned i = 0; i < palData.size(); i++) { palette->colors[i] = SDL_Color { palData[i].r, palData[i].g, palData[i].b, SDL_ALPHA_OPAQUE }; } return palette; } std::vector ReadFile(const std::string &path) { SDL_IOStream *rwops = SDL_IOFromFile(path.c_str(), "rb"); std::vector result; if (rwops == nullptr) return result; const size_t size = static_cast(SDL_GetIOSize(rwops)); result.resize(size); SDL_ReadIO(rwops, result.data(), size); SDL_CloseIO(rwops); return result; } void DrawWithBorder(const Surface &out, const Rectangle &area, tl::function_ref fn) { const uint8_t debugColor = PAL8_RED; DrawHorizontalLine(out, area.position, area.size.width, debugColor); DrawHorizontalLine(out, area.position + Displacement { 0, area.size.height - 1 }, area.size.width, debugColor); DrawVerticalLine(out, area.position, area.size.height, debugColor); DrawVerticalLine(out, area.position + Displacement { area.size.width - 1, 0 }, area.size.height, debugColor); fn(Rectangle { Point { area.position.x + 1, area.position.y + 1 }, Size { area.size.width - 2, area.size.height - 2 } }); } MATCHER_P(FileContentsEq, expectedPath, StrCat(negation ? "doesn't have" : "has", " the same contents as ", ::testing::PrintToString(expectedPath))) { if (ReadFile(arg) != ReadFile(expectedPath)) { if (UpdateExpected) { CopyFileOverwrite(arg.c_str(), expectedPath.c_str()); std::clog << "⬆️ Updated expected file at " << expectedPath << std::endl; return true; } return false; } return true; } class TextRenderIntegrationTest : public ::testing::TestWithParam { public: static void SetUpTestSuite() { palette = LoadPalette(); } static void TearDownTestSuite() { palette = nullptr; } protected: static SDLPaletteUniquePtr palette; }; SDLPaletteUniquePtr TextRenderIntegrationTest::palette; TEST_P(TextRenderIntegrationTest, RenderAndCompareTest) { const TestFixture &fixture = GetParam(); OwnedSurface out = OwnedSurface { fixture.width + 20, fixture.height + 20 }; SDL_SetSurfacePalette(out.surface, palette.get()); ASSERT_NE(out.surface, nullptr); DrawWithBorder(out, Rectangle { Point { 10, 10 }, Size { fixture.width, fixture.height } }, [&](const Rectangle &rect) { if (fixture.args.empty()) { DrawString(out, fixture.fmt, rect, fixture.opts); } else { DrawStringWithColors(out, fixture.fmt, fixture.args, rect, fixture.opts); } }); const std::string actualPath = StrCat(paths::BasePath(), FixturesPath, GetParam().name, "-Actual.png"); const std::string expectedPath = StrCat(paths::BasePath(), FixturesPath, GetParam().name, ".png"); SDL_IOStream *actual = SDL_IOFromFile(actualPath.c_str(), "wb"); ASSERT_NE(actual, nullptr) << SDL_GetError(); const tl::expected result = WriteSurfaceToFilePng(out, actual); ASSERT_TRUE(result.has_value()) << result.error(); EXPECT_THAT(actualPath, FileContentsEq(expectedPath)); } INSTANTIATE_TEST_SUITE_P(GoldenTests, TextRenderIntegrationTest, testing::ValuesIn(Fixtures), [](const testing::TestParamInfo &info) { std::string name = info.param.name; std::replace(name.begin(), name.end(), '-', '_'); return name; }); } // namespace } // namespace devilution int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); if (argc >= 2) { for (int i = 1; i < argc; ++i) { if (argv[i] != std::string_view("--update_expected")) { std::cerr << "unknown argument: " << argv[i] << "\nUsage: " << argv[0] << " [--update_expected]" << "\n"; return 64; } } UpdateExpected = true; } return RUN_ALL_TESTS(); }