From bf2f053f166ed17730d94fa51a3fb786cd4d6ace Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Thu, 19 Feb 2026 23:02:14 +0100 Subject: [PATCH 1/7] chore(filters): improve test coverage --- tests/src/Filters.cpp | 153 +++++++++++++++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 31 deletions(-) diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index d0b91cc6693..be088b32910 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -20,11 +20,13 @@ #include "providers/ffz/FfzBadges.hpp" #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/TwitchBadge.hpp" +#include "providers/twitch/TwitchBadges.hpp" #include "Test.hpp" #include #include +using namespace Qt::Literals; using namespace chatterino; using namespace chatterino::filters; using chatterino::mock::MockChannel; @@ -89,6 +91,11 @@ class MockApplication : public mock::BaseApplication return &this->logging; } + TwitchBadges *getTwitchBadges() override + { + return &this->twitchBadges; + } + mock::EmptyLogging logging; AccountController accounts; mock::EmoteController emotes; @@ -99,6 +106,7 @@ class MockApplication : public mock::BaseApplication BttvBadges bttvBadges; SeventvBadges seventvBadges; HighlightController highlights; + TwitchBadges twitchBadges; }; class FiltersF : public ::testing::Test @@ -131,15 +139,26 @@ std::ostream &operator<<(std::ostream &os, Type t) TEST(Filters, Validity) { - struct TestCase { - QString input; - bool valid; - }; - - // clang-format off - std::vector tests{ + std::vector> tests{ {"", false}, {R".(1 + 1).", true}, + {R".(1 + (1==1)).", false}, + {R".((1==1) + 1).", false}, + {R".(1 + (1 + (1==1))).", false}, + {R".(1 % "").", false}, + {R".("" % 1).", false}, + {R".(1 - "").", false}, + {R".("" - 1).", false}, + {R".(1 * "").", false}, + {R".("" * 1).", false}, + {R".(1 / "").", false}, + {R".("" / 1).", false}, + {R".("" || (1==1)).", false}, + {R".((1!=1) && 1).", false}, + {R".(1 < "").", false}, + {R".("" > 1).", false}, + {R".("" >= 1).", false}, + {R".(author.badges <= 1).", false}, {R".(1 + ).", false}, {R".(1 + 1)).", false}, {R".((1 + 1).", false}, @@ -151,9 +170,32 @@ TEST(Filters, Validity) {R".(123 + "abc" == "hello").", false}, {R".(flags.reply && flags.automod).", true}, {R".(unknown.identifier).", false}, - {R".(channel.name == "forsen" && author.badges contains "moderator").", true}, + {R".(10 startswith 1).", false}, + {R".(10 startswith "").", false}, + {R".("10" endswith 1).", false}, + {R".(1 contains "").", false}, + {R".("10" contains 1).", false}, + {R".((1+"") contains 1).", false}, + {R".("10" match 1).", false}, + {R".(1 match r"1").", false}, + { + R".(channel.name == "forsen" && author.badges contains "moderator").", + true, + }, + {R".({(1+""), 2}).", false}, + {R".("abc" match {ri"foo", "bar"}).", false}, + {R".(!{}).", false}, + {R".(!(1+"")).", false}, + {R".({).", false}, + {R".({,).", false}, + {R".({1!).", false}, + {R".((1) "").", false}, + {R".(().", false}, + {R".()").", false}, + {R".((1()").", false}, + {R".("foo).", false}, + {R".(foo").", false}, }; - // clang-format on for (const auto &[input, expected] : tests) { @@ -204,11 +246,6 @@ TEST(Filters, TypeSynthesis) TEST(Filters, Evaluation) { - struct TestCase { - QString input; - QVariant output; - }; - ContextMap contextMap = { {"author.name", QVariant("icelys")}, {"author.color", QVariant(QColor("#ff0000"))}, @@ -219,15 +256,16 @@ TEST(Filters, Evaluation) {"author.external_badges", QStringList{"frankerfacez:bot"}}, }; - // clang-format off - std::vector tests - { + std::vector> tests{ // Evaluation semantics {R".(1 + 1).", QVariant(2)}, {R".(!(1 == 1)).", QVariant(false)}, - {R".(2 + 3 * 4).", QVariant(20)}, // math operators have the same precedence + {R".(2 + 3 * 4).", + QVariant(20)}, // math operators have the same precedence {R".(1 > 2 || 3 >= 3).", QVariant(true)}, {R".(1 > 2 && 3 > 1).", QVariant(false)}, + {R".(1 > 0 && 3 > 1).", QVariant(true)}, + {R".(0 <= 0 && 3 < 1).", QVariant(false)}, {R".("abc" + 123).", QVariant("abc123")}, {R".("abc" + "456").", QVariant("abc456")}, {R".(3 - 4).", QVariant(-1)}, @@ -238,14 +276,21 @@ TEST(Filters, Evaluation) {R".(5 == "5").", QVariant(true)}, {R".(5 != 7).", QVariant(true)}, {R".(5 == "abc").", QVariant(false)}, - {R".("ABC123" == "abc123").", QVariant(true)}, // String comparison is case-insensitive + // String comparison is case-insensitive + {R".("ABC123" == "abc123").", QVariant(true)}, + {R".("ABC123" != "abc123").", QVariant(false)}, {R".("Hello world" contains "Hello").", QVariant(true)}, - {R".("Hello world" contains "LLO W").", QVariant(true)}, // Case-insensitive + {R".("Hello world" contains "LLO W").", + QVariant(true)}, // Case-insensitive {R".({"abc", "def"} contains "abc").", QVariant(true)}, - {R".({"abc", "def"} contains "ABC").", QVariant(true)}, // Case-insensitive when list is all strings - {R".({123, "def"} contains "DEF").", QVariant(false)}, // Case-sensitive if list not all strings + {R".({"abc", "def"} contains "ABC").", + QVariant(true)}, // Case-insensitive when list is all strings + {R".({123, "def"} contains "DEF").", + QVariant(false)}, // Case-sensitive if list not all strings {R".({"a123", "b456"} startswith "a123").", QVariant(true)}, {R".({"a123", "b456"} startswith "A123").", QVariant(true)}, + {R".({"a123", 1} startswith "A123").", QVariant(false)}, + {R".({"a123", 1} startswith "a123").", QVariant(true)}, {R".({} startswith "A123").", QVariant(false)}, {R".("Hello world" startswith "Hello").", QVariant(true)}, {R".("Hello world" startswith "world").", QVariant(false)}, @@ -257,13 +302,16 @@ TEST(Filters, Evaluation) {R".(author.name).", QVariant("icelys")}, {R".(!author.subbed).", QVariant(true)}, {R".(author.color == "#ff0000").", QVariant(true)}, - {R".(channel.name == "forsen" && author.badges contains "moderator").", QVariant(true)}, - {R".(author.external_badges contains "frankerfacez:bot").", QVariant(true)}, - {R".(message.content match {r"(\d\d\d\d)\-(\d\d)\-(\d\d)", 3}).", QVariant("19")}, + {R".(channel.name == "forsen" && author.badges contains "moderator").", + QVariant(true)}, + {R".(author.external_badges contains "frankerfacez:bot").", + QVariant(true)}, + {R".(message.content match {r"(\d\d\d\d)\-(\d\d)\-(\d\d)", 3}).", + QVariant("19")}, + {R".(message.content match {r"forsen", 3}).", QVariant("")}, {R".(message.content match r"HEY THERE").", QVariant(false)}, {R".(message.content match ri"HEY THERE").", QVariant(true)}, }; - // clang-format on for (const auto &[input, expected] : tests) { @@ -284,7 +332,7 @@ TEST(Filters, Evaluation) TEST_F(FiltersF, TypingContextChecks) { - MockChannel channel("pajlada"); + TwitchChannel channel("pajlada"); QByteArray message = R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=90ef1e46-8baa-4bf2-9c54-272f39d6fa11;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662206235860;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :ACTION Kappa)"; @@ -299,14 +347,57 @@ TEST_F(FiltersF, TypingContextChecks) QString originalMessage = privmsg->content(); - auto [msg, alert] = MessageBuilder::makeIrcMessage( - &channel, privmsg, MessageParseArgs{}, originalMessage, 0); + auto [msg, alert] = MessageBuilder::makeIrcMessage(&channel, privmsg, + MessageParseArgs{ + .isAction = true, + }, + originalMessage, 0); EXPECT_NE(msg.get(), nullptr); auto contextMap = buildContextMap(msg, &channel); - - EXPECT_EQ(contextMap.size(), MESSAGE_TYPING_CONTEXT.size()); + qDebug() << contextMap; + + ContextMap expected{ + {"author.badges", QStringList{"broadcaster", "subscriber", "partner"}}, + {"author.color", QColor::fromString("#CC44FF")}, + {"author.external_badges", QStringList{}}, + {"author.name", u"pajlada"_s}, + {"author.no_color", false}, + {"author.sub_length", 80}, + {"author.subbed", true}, + {"author.user_id", u"11148817"_s}, + + {"channel.live", false}, + {"channel.name", u"pajlada"_s}, + {"channel.watching", false}, + + {"flags.action", true}, + {"flags.automod", false}, + {"flags.cheer_message", false}, + {"flags.elevated_message", false}, + {"flags.first_message", false}, + {"flags.highlighted", false}, + {"flags.hype_chat", false}, + {"flags.monitored", false}, + {"flags.points_redeemed", false}, + {"flags.reply", false}, + {"flags.restricted", false}, + {"flags.reward_message", false}, + {"flags.shared", false}, + {"flags.similar", false}, + {"flags.sub_message", false}, + {"flags.system_message", false}, + {"flags.whisper", false}, + + {"message.content", "Kappa"}, + {"message.length", 5}, + + {"reward.cost", -1}, + {"reward.id", QString{}}, + {"reward.title", QString{}}, + }; + EXPECT_EQ(contextMap, expected); delete privmsg; } From 111c5238b17ce2fc5548f75b01cc018e2cfd9e25 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Fri, 20 Feb 2026 15:05:37 +0100 Subject: [PATCH 2/7] refactor(benchmark): move recent messages app/bench to helper --- benchmarks/CMakeLists.txt | 1 + benchmarks/src/MessageBuilding.cpp | 86 +++++++++++ benchmarks/src/MessageBuilding.hpp | 147 ++++++++++++++++++ benchmarks/src/RecentMessages.cpp | 232 ++--------------------------- 4 files changed, 245 insertions(+), 221 deletions(-) create mode 100644 benchmarks/src/MessageBuilding.cpp create mode 100644 benchmarks/src/MessageBuilding.hpp diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 1abc7e60833..5d7e438dfc7 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -10,6 +10,7 @@ set(benchmark_SOURCES src/LimitedQueue.cpp src/LinkParser.cpp src/RecentMessages.cpp + src/MessageBuilding.cpp # Add your new file above this line! ) diff --git a/benchmarks/src/MessageBuilding.cpp b/benchmarks/src/MessageBuilding.cpp new file mode 100644 index 00000000000..a98a7439862 --- /dev/null +++ b/benchmarks/src/MessageBuilding.cpp @@ -0,0 +1,86 @@ +#include "MessageBuilding.hpp" + +#include "messages/Emote.hpp" + +#include +#include + +namespace { + +using namespace Qt::Literals; + +std::optional tryReadJsonFile(const QString &path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + return std::nullopt; + } + + QJsonParseError e; + auto doc = QJsonDocument::fromJson(file.readAll(), &e); + if (e.error != QJsonParseError::NoError) + { + return std::nullopt; + } + + return doc; +} + +QJsonDocument readJsonFile(const QString &path) +{ + auto opt = tryReadJsonFile(path); + if (!opt) + { + _exit(1); + } + return *opt; +} + +} // namespace + +namespace chatterino::bench { + +MockMessageApplication::MockMessageApplication() + : highlights(this->settings, &this->accounts) +{ +} + +MessageBenchmark::MessageBenchmark(QString name) + : name(std::move(name)) + , chan(std::make_shared(this->name)) +{ + const auto seventvEmotes = + tryReadJsonFile(u":/bench/seventvemotes-%1.json"_s.arg(this->name)); + const auto bttvEmotes = + tryReadJsonFile(u":/bench/bttvemotes-%1.json"_s.arg(this->name)); + const auto ffzEmotes = + tryReadJsonFile(u":/bench/ffzemotes-%1.json"_s.arg(this->name)); + + if (seventvEmotes) + { + this->chan->setSeventvEmotes(std::make_shared( + seventv::detail::parseEmotes(seventvEmotes->object()["emote_set"_L1] + .toObject()["emotes"_L1] + .toArray(), + false))); + } + + if (bttvEmotes) + { + this->chan->setBttvEmotes( + std::make_shared(bttv::detail::parseChannelEmotes( + bttvEmotes->object(), this->name))); + } + + if (ffzEmotes) + { + this->chan->setFfzEmotes(std::make_shared( + ffz::detail::parseChannelEmotes(ffzEmotes->object()))); + } + + this->messages = + readJsonFile(u":/bench/recentmessages-%1.json"_s.arg(this->name)); +} + +} // namespace chatterino::bench diff --git a/benchmarks/src/MessageBuilding.hpp b/benchmarks/src/MessageBuilding.hpp new file mode 100644 index 00000000000..4577e0150c9 --- /dev/null +++ b/benchmarks/src/MessageBuilding.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include "controllers/accounts/AccountController.hpp" +#include "controllers/highlights/HighlightController.hpp" +#include "mocks/BaseApplication.hpp" +#include "mocks/EmoteController.hpp" +#include "mocks/LinkResolver.hpp" +#include "mocks/Logging.hpp" +#include "mocks/TwitchIrcServer.hpp" +#include "mocks/UserData.hpp" +#include "providers/bttv/BttvBadges.hpp" +#include "providers/chatterino/ChatterinoBadges.hpp" +#include "providers/seventv/SeventvBadges.hpp" +#include "providers/twitch/TwitchBadges.hpp" + +#include + +namespace chatterino::bench { + +class MockMessageApplication : public mock::BaseApplication +{ +public: + MockMessageApplication(); + + EmoteController *getEmotes() override + { + return &this->emotes; + } + + IUserDataController *getUserData() override + { + return &this->userData; + } + + AccountController *getAccounts() override + { + return &this->accounts; + } + + ITwitchIrcServer *getTwitch() override + { + return &this->twitch; + } + + ChatterinoBadges *getChatterinoBadges() override + { + return &this->chatterinoBadges; + } + + FfzBadges *getFfzBadges() override + { + return &this->ffzBadges; + } + + BttvBadges *getBttvBadges() override + { + return &this->bttvBadges; + } + + SeventvBadges *getSeventvBadges() override + { + return &this->seventvBadges; + } + + HighlightController *getHighlights() override + { + return &this->highlights; + } + + TwitchBadges *getTwitchBadges() override + { + return &this->twitchBadges; + } + + BttvEmotes *getBttvEmotes() override + { + return &this->bttvEmotes; + } + + FfzEmotes *getFfzEmotes() override + { + return &this->ffzEmotes; + } + + SeventvEmotes *getSeventvEmotes() override + { + return &this->seventvEmotes; + } + + IStreamerMode *getStreamerMode() override + { + return &this->streamerMode; + } + + ILinkResolver *getLinkResolver() override + { + return &this->linkResolver; + } + + ILogging *getChatLogger() override + { + return &this->logging; + } + + mock::EmptyLogging logging; + AccountController accounts; + mock::EmoteController emotes; + mock::UserDataController userData; + mock::MockTwitchIrcServer twitch; + mock::EmptyLinkResolver linkResolver; + ChatterinoBadges chatterinoBadges; + FfzBadges ffzBadges; + BttvBadges bttvBadges; + SeventvBadges seventvBadges; + HighlightController highlights; + TwitchBadges twitchBadges; + BttvEmotes bttvEmotes; + FfzEmotes ffzEmotes; + SeventvEmotes seventvEmotes; + DisabledStreamerMode streamerMode; +}; + +class MessageBenchmark +{ +public: + explicit MessageBenchmark(QString name); + + virtual ~MessageBenchmark() + { + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + + MessageBenchmark(const MessageBenchmark &) = delete; + MessageBenchmark(MessageBenchmark &&) = delete; + MessageBenchmark &operator=(const MessageBenchmark &) = delete; + MessageBenchmark &operator=(MessageBenchmark &&) = delete; + + virtual void run(benchmark::State &state) = 0; + +protected: + QString name; + MockMessageApplication app; + std::shared_ptr chan; + QJsonDocument messages; +}; + +} // namespace chatterino::bench diff --git a/benchmarks/src/RecentMessages.cpp b/benchmarks/src/RecentMessages.cpp index 3541e9d9ea1..7aed8d94dc7 100644 --- a/benchmarks/src/RecentMessages.cpp +++ b/benchmarks/src/RecentMessages.cpp @@ -3,27 +3,8 @@ // SPDX-License-Identifier: MIT #include "common/Literals.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "controllers/highlights/HighlightController.hpp" -#include "messages/Emote.hpp" -#include "mocks/BaseApplication.hpp" -#include "mocks/DisabledStreamerMode.hpp" -#include "mocks/EmoteController.hpp" -#include "mocks/LinkResolver.hpp" -#include "mocks/Logging.hpp" -#include "mocks/TwitchIrcServer.hpp" -#include "mocks/UserData.hpp" -#include "providers/bttv/BttvBadges.hpp" -#include "providers/bttv/BttvEmotes.hpp" -#include "providers/chatterino/ChatterinoBadges.hpp" -#include "providers/ffz/FfzBadges.hpp" -#include "providers/ffz/FfzEmotes.hpp" +#include "MessageBuilding.hpp" #include "providers/recentmessages/Impl.hpp" -#include "providers/seventv/SeventvBadges.hpp" -#include "providers/seventv/SeventvEmotes.hpp" -#include "providers/twitch/TwitchBadges.hpp" -#include "providers/twitch/TwitchChannel.hpp" -#include "singletons/Resources.hpp" #include #include @@ -31,211 +12,20 @@ #include #include -#include - using namespace chatterino; -using namespace literals; +using namespace Qt::Literals; namespace { -class MockApplication : public mock::BaseApplication -{ -public: - MockApplication() - : highlights(this->settings, &this->accounts) - { - } - - EmoteController *getEmotes() override - { - return &this->emotes; - } - - IUserDataController *getUserData() override - { - return &this->userData; - } - - AccountController *getAccounts() override - { - return &this->accounts; - } - - ITwitchIrcServer *getTwitch() override - { - return &this->twitch; - } - - ChatterinoBadges *getChatterinoBadges() override - { - return &this->chatterinoBadges; - } - - FfzBadges *getFfzBadges() override - { - return &this->ffzBadges; - } - - BttvBadges *getBttvBadges() override - { - return &this->bttvBadges; - } - - SeventvBadges *getSeventvBadges() override - { - return &this->seventvBadges; - } - - HighlightController *getHighlights() override - { - return &this->highlights; - } - - TwitchBadges *getTwitchBadges() override - { - return &this->twitchBadges; - } - - BttvEmotes *getBttvEmotes() override - { - return &this->bttvEmotes; - } - - FfzEmotes *getFfzEmotes() override - { - return &this->ffzEmotes; - } - - SeventvEmotes *getSeventvEmotes() override - { - return &this->seventvEmotes; - } - - IStreamerMode *getStreamerMode() override - { - return &this->streamerMode; - } - - ILinkResolver *getLinkResolver() override - { - return &this->linkResolver; - } - - ILogging *getChatLogger() override - { - return &this->logging; - } - - mock::EmptyLogging logging; - AccountController accounts; - mock::EmoteController emotes; - mock::UserDataController userData; - mock::MockTwitchIrcServer twitch; - mock::EmptyLinkResolver linkResolver; - ChatterinoBadges chatterinoBadges; - FfzBadges ffzBadges; - BttvBadges bttvBadges; - SeventvBadges seventvBadges; - HighlightController highlights; - TwitchBadges twitchBadges; - BttvEmotes bttvEmotes; - FfzEmotes ffzEmotes; - SeventvEmotes seventvEmotes; - DisabledStreamerMode streamerMode; -}; - -std::optional tryReadJsonFile(const QString &path) -{ - QFile file(path); - if (!file.open(QFile::ReadOnly)) - { - return std::nullopt; - } - - QJsonParseError e; - auto doc = QJsonDocument::fromJson(file.readAll(), &e); - if (e.error != QJsonParseError::NoError) - { - return std::nullopt; - } - - return doc; -} - -QJsonDocument readJsonFile(const QString &path) -{ - auto opt = tryReadJsonFile(path); - if (!opt) - { - _exit(1); - } - return *opt; -} - -class RecentMessages -{ -public: - explicit RecentMessages(const QString &name_) - : name(name_) - , chan(this->name) - { - const auto seventvEmotes = - tryReadJsonFile(u":/bench/seventvemotes-%1.json"_s.arg(this->name)); - const auto bttvEmotes = - tryReadJsonFile(u":/bench/bttvemotes-%1.json"_s.arg(this->name)); - const auto ffzEmotes = - tryReadJsonFile(u":/bench/ffzemotes-%1.json"_s.arg(this->name)); - - if (seventvEmotes) - { - this->chan.setSeventvEmotes( - std::make_shared(seventv::detail::parseEmotes( - seventvEmotes->object()["emote_set"_L1] - .toObject()["emotes"_L1] - .toArray(), - false))); - } - - if (bttvEmotes) - { - this->chan.setBttvEmotes(std::make_shared( - bttv::detail::parseChannelEmotes(bttvEmotes->object(), - this->name))); - } - - if (ffzEmotes) - { - this->chan.setFfzEmotes(std::make_shared( - ffz::detail::parseChannelEmotes(ffzEmotes->object()))); - } - - this->messages = - readJsonFile(u":/bench/recentmessages-%1.json"_s.arg(this->name)); - } - - ~RecentMessages() - { - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - } - - virtual void run(benchmark::State &state) = 0; - -protected: - QString name; - MockApplication app; - TwitchChannel chan; - QJsonDocument messages; -}; - -class ParseRecentMessages : public RecentMessages +class ParseRecentMessages : public bench::MessageBenchmark { public: - explicit ParseRecentMessages(const QString &name_) - : RecentMessages(name_) + explicit ParseRecentMessages(QString name) + : bench::MessageBenchmark(std::move(name)) { } - void run(benchmark::State &state) + void run(benchmark::State &state) override { for (auto _ : state) { @@ -246,22 +36,22 @@ class ParseRecentMessages : public RecentMessages } }; -class BuildRecentMessages : public RecentMessages +class BuildRecentMessages : public bench::MessageBenchmark { public: - explicit BuildRecentMessages(const QString &name_) - : RecentMessages(name_) + explicit BuildRecentMessages(QString name) + : bench::MessageBenchmark(std::move(name)) { } - void run(benchmark::State &state) + void run(benchmark::State &state) override { auto parsed = recentmessages::detail::parseRecentMessages( this->messages.object()); for (auto _ : state) { auto built = recentmessages::detail::buildRecentMessages( - parsed, &this->chan); + parsed, this->chan.get()); benchmark::DoNotOptimize(built); } } From b33e4566a44d3813733ee4566cea1cd585df8b75 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Fri, 20 Feb 2026 15:55:07 +0100 Subject: [PATCH 3/7] chore(filters): Add benchmarks --- benchmarks/CMakeLists.txt | 1 + benchmarks/src/Filters.cpp | 129 ++++++++++++++++++++++++++ src/controllers/filters/FilterSet.cpp | 5 + src/controllers/filters/FilterSet.hpp | 1 + 4 files changed, 136 insertions(+) create mode 100644 benchmarks/src/Filters.cpp diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 5d7e438dfc7..7442bce25da 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -11,6 +11,7 @@ set(benchmark_SOURCES src/LinkParser.cpp src/RecentMessages.cpp src/MessageBuilding.cpp + src/Filters.cpp # Add your new file above this line! ) diff --git a/benchmarks/src/Filters.cpp b/benchmarks/src/Filters.cpp new file mode 100644 index 00000000000..52ceb6b5276 --- /dev/null +++ b/benchmarks/src/Filters.cpp @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2026 Contributors to Chatterino +// +// SPDX-License-Identifier: MIT + +#include "common/Literals.hpp" +#include "controllers/filters/FilterSet.hpp" +#include "MessageBuilding.hpp" +#include "providers/recentmessages/Impl.hpp" + +#include +#include +#include +#include +#include + +using namespace chatterino; +using namespace Qt::Literals; + +namespace { + +class FilterMessages : public bench::MessageBenchmark +{ +public: + explicit FilterMessages(QString name, QStringList filters) + : bench::MessageBenchmark(std::move(name)) + , filterTexts(std::move(filters)) + { + } + + void run(benchmark::State &state) override + { + auto parsed = recentmessages::detail::parseRecentMessages( + this->messages.object()); + auto built = recentmessages::detail::buildRecentMessages( + parsed, this->chan.get()); + + QMap filters; + for (qsizetype i = 0; i < this->filterTexts.size(); i++) + { + // ensure deterministic order + auto id = QUuid(static_cast(i), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + auto filter = std::make_shared( + QString::number(i), this->filterTexts[i], id); + if (!filter->valid()) + { + qCDebug(chatterinoApp) << i << this->filterTexts[i]; + assert(false); + continue; + } + filters.insert(id, filter); + } + assert(filters.size() == this->filterTexts.size()); + FilterSet set(std::move(filters)); + + for (auto _ : state) + { + for (const auto &msg : built) + { + bool filtered = set.filter(msg, this->chan); + benchmark::DoNotOptimize(filtered); + benchmark::ClobberMemory(); + } + } + } + +private: + QStringList filterTexts; +}; + +void BM_FilterMessages(benchmark::State &state, QString channel, + QStringList filters) +{ + FilterMessages bench(std::move(channel), std::move(filters)); + bench.run(state); +} + +} // namespace + +BENCHMARK_CAPTURE( + BM_FilterMessages, nymn_modmessages, u"nymn"_s, + { + uR".(channel.name == "nymn" && author.badges contains "moderator")."_s, + }); + +BENCHMARK_CAPTURE( + BM_FilterMessages, nymn_mod_party, u"nymn"_s, + { + uR".((author.badges contains "moderator") && (message.content contains "forsenParty"))."_s, + }); + +BENCHMARK_CAPTURE(BM_FilterMessages, nymn_len40_or_sub, u"nymn"_s, + { + uR".(message.length < 40 || author.subbed)."_s, + }); + +BENCHMARK_CAPTURE(BM_FilterMessages, nymn_no_sub, u"nymn"_s, + { + uR".(!flags.sub_message)."_s, + }); + +BENCHMARK_CAPTURE(BM_FilterMessages, nymn_with_color, u"nymn"_s, + { + uR".(!author.no_color)."_s, + }); + +BENCHMARK_CAPTURE( + BM_FilterMessages, nymn_complex_regex, u"nymn"_s, + { + uR".((message.content match r"^(?!.*(?:my|complex|(re.*x))).*$"))."_s, + }); + +BENCHMARK_CAPTURE( + BM_FilterMessages, nymn_big_or, u"nymn"_s, + { + uR".((author.subbed && author.sub_length >= 6) || flags.system_message || flags.first_message || flags.automod || flags.sub_message)."_s, + }); + +BENCHMARK_CAPTURE( + BM_FilterMessages, nymn_with_color_and_edm_single, u"nymn"_s, + { + uR".(!author.no_color && message.content contains "EDM")."_s, + }); + +BENCHMARK_CAPTURE(BM_FilterMessages, nymn_with_color_and_edm_separate, + u"nymn"_s, + { + uR".(!author.no_color)."_s, + uR".(message.content contains "EDM")."_s, + }); diff --git a/src/controllers/filters/FilterSet.cpp b/src/controllers/filters/FilterSet.cpp index b2566f71abf..1eca1f942ff 100644 --- a/src/controllers/filters/FilterSet.cpp +++ b/src/controllers/filters/FilterSet.cpp @@ -34,6 +34,11 @@ FilterSet::FilterSet(const QList &filterIds) }); } +FilterSet::FilterSet(QMap filters) + : filters_(std::move(filters)) +{ +} + FilterSet::~FilterSet() { this->listener_.disconnect(); diff --git a/src/controllers/filters/FilterSet.hpp b/src/controllers/filters/FilterSet.hpp index eb66d5b106e..d17b8939f4d 100644 --- a/src/controllers/filters/FilterSet.hpp +++ b/src/controllers/filters/FilterSet.hpp @@ -25,6 +25,7 @@ class FilterSet public: FilterSet(); FilterSet(const QList &filterIds); + FilterSet(QMap filters); ~FilterSet(); From e7d2f45ba8afb49c9c0a09a08487b01c0177f083 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Fri, 20 Feb 2026 16:17:15 +0100 Subject: [PATCH 4/7] Update benchmarks/src/Filters.cpp Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- benchmarks/src/Filters.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/src/Filters.cpp b/benchmarks/src/Filters.cpp index 52ceb6b5276..11cf1e4f2c9 100644 --- a/benchmarks/src/Filters.cpp +++ b/benchmarks/src/Filters.cpp @@ -40,7 +40,7 @@ class FilterMessages : public bench::MessageBenchmark // ensure deterministic order auto id = QUuid(static_cast(i), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); auto filter = std::make_shared( - QString::number(i), this->filterTexts[i], id); + QString::number(i), this->filterTexts.at(i), id); if (!filter->valid()) { qCDebug(chatterinoApp) << i << this->filterTexts[i]; From 5879c121fb9eb4205a9676ba46a2a95f5930b942 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Fri, 20 Feb 2026 16:17:20 +0100 Subject: [PATCH 5/7] Update tests/src/Filters.cpp Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- tests/src/Filters.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index be088b32910..00d613289cb 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -360,7 +360,7 @@ TEST_F(FiltersF, TypingContextChecks) ContextMap expected{ {"author.badges", QStringList{"broadcaster", "subscriber", "partner"}}, - {"author.color", QColor::fromString("#CC44FF")}, + {"author.color", QColor(0xCC44FF)}, {"author.external_badges", QStringList{}}, {"author.name", u"pajlada"_s}, {"author.no_color", false}, From 9bd6344ebf85839f81b86c656fefdad115a2ec53 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 21 Feb 2026 10:52:43 +0100 Subject: [PATCH 6/7] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 758a9dffd4a..8da00969c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ - Dev: Balance IPv4 and IPv6 connection attempts. (#6804) - Dev: Factored out recent messages benchmark helper. (#6815) - Dev: Added `CHATTERINO_FORCE_LTO` CMake option to skip LTO check. (#6816) +- Dev: Added more tests and benchmarks for filters. (#6814) ## 2.5.4 From 1caafb0328b0534b8491539d1dc3b4532ecd0357 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 21 Feb 2026 12:20:14 +0100 Subject: [PATCH 7/7] fix: use existing ctor --- benchmarks/src/Filters.cpp | 8 +++++--- benchmarks/src/MessageBuilding.cpp | 1 + src/controllers/filters/FilterSet.cpp | 5 ----- src/controllers/filters/FilterSet.hpp | 1 - 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/benchmarks/src/Filters.cpp b/benchmarks/src/Filters.cpp index 11cf1e4f2c9..c3e8e7de886 100644 --- a/benchmarks/src/Filters.cpp +++ b/benchmarks/src/Filters.cpp @@ -34,7 +34,7 @@ class FilterMessages : public bench::MessageBenchmark auto built = recentmessages::detail::buildRecentMessages( parsed, this->chan.get()); - QMap filters; + QList filters; for (qsizetype i = 0; i < this->filterTexts.size(); i++) { // ensure deterministic order @@ -47,10 +47,12 @@ class FilterMessages : public bench::MessageBenchmark assert(false); continue; } - filters.insert(id, filter); + getSettings()->filterRecords.append(filter); + filters.append(id); } assert(filters.size() == this->filterTexts.size()); - FilterSet set(std::move(filters)); + FilterSet set(filters); + assert(set.filterIds().size() == filters.size()); for (auto _ : state) { diff --git a/benchmarks/src/MessageBuilding.cpp b/benchmarks/src/MessageBuilding.cpp index 1e96096c644..8ab6b89001a 100644 --- a/benchmarks/src/MessageBuilding.cpp +++ b/benchmarks/src/MessageBuilding.cpp @@ -48,6 +48,7 @@ namespace chatterino::bench { MockMessageApplication::MockMessageApplication() : highlights(this->settings, &this->accounts) { + this->settings.disableSave(); } MessageBenchmark::MessageBenchmark(QString name) diff --git a/src/controllers/filters/FilterSet.cpp b/src/controllers/filters/FilterSet.cpp index 1eca1f942ff..b2566f71abf 100644 --- a/src/controllers/filters/FilterSet.cpp +++ b/src/controllers/filters/FilterSet.cpp @@ -34,11 +34,6 @@ FilterSet::FilterSet(const QList &filterIds) }); } -FilterSet::FilterSet(QMap filters) - : filters_(std::move(filters)) -{ -} - FilterSet::~FilterSet() { this->listener_.disconnect(); diff --git a/src/controllers/filters/FilterSet.hpp b/src/controllers/filters/FilterSet.hpp index d17b8939f4d..eb66d5b106e 100644 --- a/src/controllers/filters/FilterSet.hpp +++ b/src/controllers/filters/FilterSet.hpp @@ -25,7 +25,6 @@ class FilterSet public: FilterSet(); FilterSet(const QList &filterIds); - FilterSet(QMap filters); ~FilterSet();