You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

456 lines
13 KiB

#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <string>
#include <string_view>
#include <variant>
#ifdef USE_SDL3
#include <SDL3/SDL_iostream.h>
#include <SDL3/SDL_surface.h>
#else
#include <SDL.h>
#endif
#include <expected.hpp>
#include <function_ref.hpp>
#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/png.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<DrawStringFormatArg> 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 },
},
},
TestFixture {
.name = "cursor-start",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 0, // Cursor at start
.cursorStatic = true,
},
},
TestFixture {
.name = "cursor-middle",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 5, // Cursor after "Hello",
.cursorStatic = true,
},
},
TestFixture {
.name = "cursor-end",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 11, // Cursor at end
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-end_first_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 10, // Cursor at end of first line
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-start_second_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 11, // Cursor at start of second line
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-middle_second_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 14, // Cursor at second line, at the 'o' of "Second"
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-end_second_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 22, // Cursor at start of second line
.cursorStatic = true,
},
},
TestFixture {
.name = "highlight-partial",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.highlightRange = { 5, 10 }, // Highlight " Worl"
.highlightColor = PAL8_BLUE,
},
},
TestFixture {
.name = "highlight-full",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.highlightRange = { 0, 11 }, // Highlight entire text
.highlightColor = PAL8_BLUE,
},
},
TestFixture {
.name = "multiline_highlight",
.width = 70,
.height = 50,
.fmt = "Hello\nWorld",
.opts = {
.flags = UiFlags::ColorUiGold,
.highlightRange = { 3, 8 }, // Highlight "lo\nWo"
.highlightColor = PAL8_BLUE,
},
},
};
SDLPaletteUniquePtr LoadPalette()
{
struct Color {
uint8_t r, g, b;
};
std::array<Color, 256> palData;
LoadFileInMem("ui_art\\diablo.pal", palData);
SDLPaletteUniquePtr palette = SDLWrap::AllocPalette(static_cast<int>(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;
}
void DrawWithBorder(const Surface &out, const Rectangle &area, tl::function_ref<void(const Rectangle &)> 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 } });
}
bool MaybeUpdateExpected(const std::string &actualPath, const std::string &expectedPath)
{
if (!UpdateExpected) return false;
CopyFileOverwrite(actualPath.c_str(), expectedPath.c_str());
std::clog << " Updated expected file at " << expectedPath << std::endl;
return true;
}
class TextRenderIntegrationTest : public ::testing::TestWithParam<TestFixture> {
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<void, std::string> result = WriteSurfaceToFilePng(out, actual);
ASSERT_TRUE(result.has_value()) << result.error();
// We compare pixels rather than PNG file contents because different
// versions of SDL may use different PNG encoders.
SDLSurfaceUniquePtr actualSurface { LoadPNG(actualPath.c_str()) };
ASSERT_NE(actualSurface, nullptr) << SDL_GetError();
SDLSurfaceUniquePtr expectedSurface { LoadPNG(expectedPath.c_str()) };
ASSERT_NE(expectedSurface, nullptr) << SDL_GetError();
ASSERT_NE(actualSurface->pixels, nullptr);
ASSERT_NE(expectedSurface->pixels, nullptr);
if ((actualSurface->h != expectedSurface->h || actualSurface->w != expectedSurface->w)
&& MaybeUpdateExpected(actualPath, expectedPath)) {
return;
}
ASSERT_EQ(actualSurface->h, expectedSurface->h);
ASSERT_EQ(actualSurface->w, expectedSurface->w);
for (int y = 0; y < expectedSurface->h; y++) {
for (int x = 0; x < expectedSurface->w; x++) {
const uint8_t actualPixel = reinterpret_cast<uint8_t *>(actualSurface->pixels)[y * actualSurface->pitch + x];
const uint8_t expectedPixel = reinterpret_cast<uint8_t *>(expectedSurface->pixels)[y * expectedSurface->pitch + x];
if (actualPixel != expectedPixel) {
if (MaybeUpdateExpected(actualPath, expectedPath)) return;
ASSERT_TRUE(false) << "Images are different at (" << x << ", " << y << ") "
<< static_cast<int>(actualPixel) << " != " << static_cast<int>(expectedPixel);
}
}
}
}
INSTANTIATE_TEST_SUITE_P(GoldenTests, TextRenderIntegrationTest,
testing::ValuesIn(Fixtures),
[](const testing::TestParamInfo<TestFixture> &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();
}