From 4db368d4741eff4d2b2942c25d5e633648e23a64 Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Tue, 19 Nov 2024 16:24:02 +0100 Subject: [PATCH] QFontEngine: add test coverage and benchmark for all engines For some known fonts, confirm that we get the right glyph count and glyph index for specific unicode code points. Create QGuiApplication with a specific font engine, and test the fonts with all engines. On Windows, that's DirectWrite, GDI, and Freetype; on macOS CoreText and Freetype; and otherwise only Freetype. Not all fonts will be available with all engines, so test in each test function whether the font is a good enough match (family is enough, no need to do a deep test). Add a benchmark as well, using the same setup plumbing, but with different test functions. Change-Id: I2ed279965fc3f1dc3f283d0fe7b018fc3035c67d Reviewed-by: Anton Kudryavtsev Reviewed-by: Eskil Abrahamsen Blomfeldt --- tests/auto/gui/text/CMakeLists.txt | 1 + .../auto/gui/text/qfontengine/CMakeLists.txt | 40 +++ .../gui/text/qfontengine/tst_qfontengine.cpp | 257 ++++++++++++++++++ tests/benchmarks/gui/text/CMakeLists.txt | 1 + .../gui/text/qfontengine/CMakeLists.txt | 18 ++ .../gui/text/qfontengine/tst_qfontengine.cpp | 33 +++ 6 files changed, 350 insertions(+) create mode 100644 tests/auto/gui/text/qfontengine/CMakeLists.txt create mode 100644 tests/auto/gui/text/qfontengine/tst_qfontengine.cpp create mode 100644 tests/benchmarks/gui/text/qfontengine/CMakeLists.txt create mode 100644 tests/benchmarks/gui/text/qfontengine/tst_qfontengine.cpp diff --git a/tests/auto/gui/text/CMakeLists.txt b/tests/auto/gui/text/CMakeLists.txt index 30b35fb10a3..3283ae00970 100644 --- a/tests/auto/gui/text/CMakeLists.txt +++ b/tests/auto/gui/text/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(qabstracttextdocumentlayout) add_subdirectory(qfont) add_subdirectory(qfontdatabase) +add_subdirectory(qfontengine) add_subdirectory(qfontmetrics) add_subdirectory(qglyphrun) add_subdirectory(qrawfont) diff --git a/tests/auto/gui/text/qfontengine/CMakeLists.txt b/tests/auto/gui/text/qfontengine/CMakeLists.txt new file mode 100644 index 00000000000..a68bc1148b8 --- /dev/null +++ b/tests/auto/gui/text/qfontengine/CMakeLists.txt @@ -0,0 +1,40 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qfontengine Test: +##################################################################### + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qfontengine LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +# Resources: +set_source_files_properties("../../../shared/resources/test.ttf" + PROPERTIES QT_RESOURCE_ALIAS "test.ttf" +) +set_source_files_properties("../../../shared/resources/testfont.ttf" + PROPERTIES QT_RESOURCE_ALIAS "testfont.ttf" +) +set_source_files_properties("../../../shared/resources/testfont_variable.ttf" + PROPERTIES QT_RESOURCE_ALIAS "testfont_variable.ttf" +) +set(testfonts_resource_files + "../../../shared/resources/test.ttf" + "../../../shared/resources/testfont.ttf" + "../../../shared/resources/testfont_variable.ttf" +) + +qt_internal_add_test(tst_qfontengine + SOURCES + tst_qfontengine.cpp + TESTDATA + ${testfonts_resource_files} + LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate + BUILTIN_TESTDATA +) diff --git a/tests/auto/gui/text/qfontengine/tst_qfontengine.cpp b/tests/auto/gui/text/qfontengine/tst_qfontengine.cpp new file mode 100644 index 00000000000..33b285f01dc --- /dev/null +++ b/tests/auto/gui/text/qfontengine/tst_qfontengine.cpp @@ -0,0 +1,257 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +#include +#include +#include +#include +#include + +using namespace Qt::StringLiterals; + +class tst_QFontEngine : public QObject +{ + Q_OBJECT + + struct Candidate { + QLatin1StringView rowName; + int expectedGlyphCount; + int ucs4; + glyph_t expectedGlyphIndex; + + QString familyName() const + { + const auto split = rowName.indexOf('@'); + Q_ASSERT(split >= 0); + return QString::fromUtf8(rowName.mid(split + 1)); + } + + QFont font() const + { + QFont font(familyName()); + font.setStyleStrategy(QFont::NoFontMerging); + return font; + } + + bool isFontAvailable() const + { + QFont theFont(font()); + return theFont.family() == QFontInfo(theFont).family(); + } + + QFontEngine *fontEngine() const + { + return QFontPrivate::get(font())->engineForScript(QChar::Script_Common); + } + }; + +public: + tst_QFontEngine() = default; + +private slots: + void initTestCase_data(); + void init(); + void cleanup(); + + void glyphCount_data() { data(); } + void glyphCount(); + void glyphIndex_data() { data(); } + void glyphIndex(); + +private: + void setupApplication(); + void data(); + + // member variables to keep the arguments data alive + QByteArray platformArgument; + QList theArguments; + std::unique_ptr theApp; + + int QtsSpecialTestFont = -1; + int QtBidiTestFont = -1; + int QtTestVariableFont = -1; +}; + +// The tst_bench_QFontEngine benchmark project shares the test class declaration +// and the common setup code here. The file is both compiled as part of the +// project (moc needs to see it), and included (so that the benchmark code sees +// the class declaration). +#if !defined(QFONTENGINE_BENCHMARK) || defined(QFONTENGINE_COMMON) + +void tst_QFontEngine::initTestCase_data() +{ + QTest::addColumn("platform"); + QTest::addColumn("engine"); + QTest::addColumn("engineType"); + +#if defined(Q_OS_WIN) + QTest::addRow("DirectWrite") << ""_ba << ""_ba << QFontEngine::DirectWrite; + QTest::addRow("GDI") << "windows"_ba << "gdi"_ba << QFontEngine::Win; + QTest::addRow("Freetype") << "windows"_ba << "freetype"_ba << QFontEngine::Freetype; +#elif defined(Q_OS_DARWIN) + QTest::addRow("CoreText") << ""_ba << ""_ba << QFontEngine::Mac; + QTest::addRow("Freetype") << "cocoa"_ba << "freetype"_ba << QFontEngine::Freetype; +#else + QTest::addRow("freetype") << ""_ba << ""_ba << QFontEngine::Freetype; +#endif +} + +void tst_QFontEngine::init() +{ + setupApplication(); + +#if !defined(QFONTENGINE_BENCHMARK) + QtsSpecialTestFont = QFontDatabase::addApplicationFont(QFINDTESTDATA("test.ttf")); + QVERIFY(QtsSpecialTestFont >= 0); + QCOMPARE(QFontDatabase::applicationFontFamilies(QtsSpecialTestFont), + QStringList{u"QtsSpecialTestFont"_s}); + QtBidiTestFont = QFontDatabase::addApplicationFont(QFINDTESTDATA("testfont.ttf")); + QVERIFY(QtBidiTestFont >= 0); + QCOMPARE(QFontDatabase::applicationFontFamilies(QtBidiTestFont), + QStringList{u"QtBidiTestFont"_s}); + // This font comes with two font faces, so we get multiple entries (which + // might be a bug, esp since on macOS we get two, on Windows we get three + // identical families). + QtTestVariableFont = QFontDatabase::addApplicationFont(QFINDTESTDATA("testfont_variable.ttf")); + QVERIFY(QtTestVariableFont >= 0); + QVERIFY(QFontDatabase::applicationFontFamilies(QtTestVariableFont) + .contains(u"QtTestVariableFont"_s)); +#endif +} + +void tst_QFontEngine::cleanup() +{ +#if !defined(QFONTENGINE_BENCHMARK) + QFontDatabase::removeApplicationFont(QtTestVariableFont); + QFontDatabase::removeApplicationFont(QtBidiTestFont); + QFontDatabase::removeApplicationFont(QtsSpecialTestFont); +#endif + + theApp.reset(); + theArguments = {}; +} + +void tst_QFontEngine::setupApplication() +{ + if (theApp) + return; + QFETCH_GLOBAL(const QByteArray, platform); + QFETCH_GLOBAL(const QByteArray, engine); + + QList arguments = {"tst_qfontengine"}; + if (!platform.isEmpty()) { + arguments += "-platform"; + platformArgument = platform + ":fontengine=" + engine; + arguments += platformArgument.data(); + } + + theArguments = arguments; + int argc = arguments.size(); + theApp = std::make_unique(argc, const_cast(theArguments.data())); +} + +// only called once per test function, even if the global data (i.e. font engine) changed! +void tst_QFontEngine::data() +{ + QTest::addColumn("candidate"); + + const std::initializer_list candidates = { +#if !defined(QFONTENGINE_BENCHMARK) + // our own testfonts + { + ".notdef@QtsSpecialTestFont"_L1, + 6, 0x0000, 0, + }, + { + "Dotty@QtsSpecialTestFont"_L1, + 6, 0xe000, 1, + }, + { + "A@QtsSpecialTestFont"_L1, + 6, 0x0041, 2, + }, + { + "one@QtsSpecialTestFont"_L1, + 6, 0x0031, 3, + }, + { + "uni200D@QtsSpecialTestFont"_L1, + 6, 0x200d, 4, + }, + { + "uniFFFC@QtsSpecialTestFont"_L1, + 6, 0xfffc, 5, + }, + { + "percent@QtBidiTestFont"_L1, + 150, 0x0025, 13, + }, + { // up arrow, beyond 0xFFFF + "u1034A@QtBidiTestFont"_L1, + 150, 0x1034A, 149, + }, + { + "peseta@QtTestVariableFont"_L1, + 235, 0x20a7, 218, + }, +#else // some typical icon fonts that might be present on the system, but also change frequently + { // early hit + "faucet@Font Awesome 6 Free"_L1, + 1399, 0xe005, 51, + }, + { // last glyph in the font + "caravan@Font Awesome 6 Free"_L1, + 1399, 0xf8ff, 1398, + }, + { + "delete@Material Symbols Outlined"_L1, + 5032, 0xe872, 1, + }, + { // the Segoe Fluent Icons font that comes with Windows 11 + "gid1486@Segoe Fluent Icons"_L1, // voicemail + 1529, 0xf47f, 1486, + }, +#endif // !defined(QFONTENGINE_BENCHMARK) + }; + + for (auto candidate : candidates) + QTest::addRow("%s", candidate.rowName.constData()) << candidate; +} + +#include "tst_qfontengine.moc" +QTEST_APPLESS_MAIN(tst_QFontEngine) + +#endif // !defined(QFONTENGINE_BENCHMARK) || defined(QFONTENGINE_COMMON) + +// The benchmark project implements the test functions differently. + +#if !defined(QFONTENGINE_BENCHMARK) + +void tst_QFontEngine::glyphCount() +{ + QFETCH_GLOBAL(const QFontEngine::Type, engineType); + QFETCH(const Candidate, candidate); + if (!candidate.isFontAvailable()) + QSKIP("Font is not available"); + + const auto fontEngine = candidate.fontEngine(); + QCOMPARE(fontEngine->type(), engineType); + QCOMPARE(fontEngine->glyphCount(), candidate.expectedGlyphCount); +} + +void tst_QFontEngine::glyphIndex() +{ + QFETCH_GLOBAL(const QFontEngine::Type, engineType); + QFETCH(const Candidate, candidate); + if (!candidate.isFontAvailable()) + QSKIP("Font is not available"); + + const auto fontEngine = candidate.fontEngine(); + QCOMPARE(fontEngine->type(), engineType); + QCOMPARE(fontEngine->glyphIndex(candidate.ucs4), candidate.expectedGlyphIndex); +} + +#endif // !define(QFONTENGINE_BENCHMARK) + diff --git a/tests/benchmarks/gui/text/CMakeLists.txt b/tests/benchmarks/gui/text/CMakeLists.txt index 052d49e03c2..05e4b873079 100644 --- a/tests/benchmarks/gui/text/CMakeLists.txt +++ b/tests/benchmarks/gui/text/CMakeLists.txt @@ -1,6 +1,7 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause +add_subdirectory(qfontengine) add_subdirectory(qfontmetrics) add_subdirectory(qtext) add_subdirectory(qtextdocument) diff --git a/tests/benchmarks/gui/text/qfontengine/CMakeLists.txt b/tests/benchmarks/gui/text/qfontengine/CMakeLists.txt new file mode 100644 index 00000000000..ddd3797a661 --- /dev/null +++ b/tests/benchmarks/gui/text/qfontengine/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_bench_QFontEngine Binary: +##################################################################### + +qt_internal_add_benchmark(tst_bench_QFontEngine + SOURCES + ../../../../auto/gui/text/qfontengine/tst_qfontengine.cpp + tst_qfontengine.cpp + DEFINES + QFONTENGINE_BENCHMARK + LIBRARIES + Qt::Gui + Qt::GuiPrivate + Qt::Test +) diff --git a/tests/benchmarks/gui/text/qfontengine/tst_qfontengine.cpp b/tests/benchmarks/gui/text/qfontengine/tst_qfontengine.cpp new file mode 100644 index 00000000000..15236d4c596 --- /dev/null +++ b/tests/benchmarks/gui/text/qfontengine/tst_qfontengine.cpp @@ -0,0 +1,33 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#define QFONTENGINE_COMMON +#include "../../../../auto/gui/text/qfontengine/tst_qfontengine.cpp" + +using namespace Qt::StringLiterals; + +void tst_QFontEngine::glyphCount() +{ + QFETCH(const Candidate, candidate); + if (!candidate.isFontAvailable()) + QSKIP("Font is not available"); + + const auto fontEngine = candidate.fontEngine(); + + QBENCHMARK { + fontEngine->glyphCount(); + } +} + +void tst_QFontEngine::glyphIndex() +{ + QFETCH(const Candidate, candidate); + if (!candidate.isFontAvailable()) + QSKIP("Font is not available"); + + const auto fontEngine = candidate.fontEngine(); + + QBENCHMARK { + fontEngine->glyphIndex(candidate.ucs4); + } +}