test: add snapshot tests for MessageBuilder (#5598)
This commit is contained in:
+506
-70
@@ -1,34 +1,63 @@
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
|
||||
#include "common/Literals.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
#include "controllers/sound/NullBackend.hpp"
|
||||
#include "lib/Snapshot.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "mocks/BaseApplication.hpp"
|
||||
#include "mocks/Channel.hpp"
|
||||
#include "mocks/ChatterinoBadges.hpp"
|
||||
#include "mocks/DisabledStreamerMode.hpp"
|
||||
#include "mocks/Emotes.hpp"
|
||||
#include "mocks/LinkResolver.hpp"
|
||||
#include "mocks/Logging.hpp"
|
||||
#include "mocks/TwitchIrcServer.hpp"
|
||||
#include "mocks/UserData.hpp"
|
||||
#include "providers/ffz/FfzBadges.hpp"
|
||||
#include "providers/seventv/SeventvBadges.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/ChannelPointReward.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "Test.hpp"
|
||||
#include "util/IrcHelpers.hpp"
|
||||
|
||||
#include <IrcConnection>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QString>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
using namespace chatterino;
|
||||
using chatterino::mock::MockChannel;
|
||||
using namespace literals;
|
||||
|
||||
namespace {
|
||||
|
||||
/// Controls whether snapshots will be updated (true) or verified (false)
|
||||
///
|
||||
/// In CI, all snapshots must be verified, thus the integrity tests checks for
|
||||
/// this constant.
|
||||
///
|
||||
/// When adding a test, start with `{ "input": "..." }` and set this to `true`
|
||||
/// to generate an initial snapshot. Make sure to verify the output!
|
||||
constexpr bool UPDATE_SNAPSHOTS = false;
|
||||
|
||||
const QString IRC_CATEGORY = u"MessageBuilder/IRC"_s;
|
||||
|
||||
class MockApplication : public mock::BaseApplication
|
||||
{
|
||||
public:
|
||||
@@ -37,6 +66,12 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
MockApplication(const QString &settingsData)
|
||||
: mock::BaseApplication(settingsData)
|
||||
, highlights(this->settings, &this->accounts)
|
||||
{
|
||||
}
|
||||
|
||||
IEmotes *getEmotes() override
|
||||
{
|
||||
return &this->emotes;
|
||||
@@ -97,6 +132,21 @@ public:
|
||||
return &this->logging;
|
||||
}
|
||||
|
||||
TwitchBadges *getTwitchBadges() override
|
||||
{
|
||||
return &this->twitchBadges;
|
||||
}
|
||||
|
||||
ILinkResolver *getLinkResolver() override
|
||||
{
|
||||
return &this->linkResolver;
|
||||
}
|
||||
|
||||
ISoundController *getSound() override
|
||||
{
|
||||
return &this->sound;
|
||||
}
|
||||
|
||||
mock::EmptyLogging logging;
|
||||
AccountController accounts;
|
||||
mock::Emotes emotes;
|
||||
@@ -109,8 +159,292 @@ public:
|
||||
BttvEmotes bttvEmotes;
|
||||
FfzEmotes ffzEmotes;
|
||||
SeventvEmotes seventvEmotes;
|
||||
TwitchBadges twitchBadges;
|
||||
mock::EmptyLinkResolver linkResolver;
|
||||
NullBackend sound;
|
||||
};
|
||||
|
||||
std::pair<const EmoteName, EmotePtr> makeEmote(Emote &&emote)
|
||||
{
|
||||
auto ptr = std::make_shared<Emote>(std::move(emote));
|
||||
ptr->homePage = {u"https://chatterino.com/" % ptr->name.string};
|
||||
ptr->tooltip = {ptr->name.string % u" Tooltip"_s};
|
||||
ptr->author = {u"Chatterino"_s};
|
||||
ptr->images = {
|
||||
Url{u"https://chatterino.com/" % ptr->name.string % u".png"}};
|
||||
return {ptr->name, ptr};
|
||||
}
|
||||
|
||||
using EmoteMapPtr = std::shared_ptr<const EmoteMap>;
|
||||
|
||||
EmoteMapPtr makeEmotes(auto &&...emotes)
|
||||
{
|
||||
auto map = std::make_shared<EmoteMap>();
|
||||
((map->emplace(makeEmote(std::forward<decltype(emotes)>(emotes)))), ...);
|
||||
return map;
|
||||
}
|
||||
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
|
||||
|
||||
struct MockEmotes {
|
||||
EmoteMapPtr seventv;
|
||||
EmoteMapPtr bttv;
|
||||
EmoteMapPtr ffz;
|
||||
EmoteMapPtr twitchAccount;
|
||||
|
||||
static MockEmotes channel()
|
||||
{
|
||||
return {
|
||||
.seventv = makeEmotes(
|
||||
Emote{
|
||||
.name = {u"7TVEmote"_s},
|
||||
.id = {u"1"_s},
|
||||
},
|
||||
Emote{
|
||||
.name = {u"7TVEmote0w"_s},
|
||||
.zeroWidth = true,
|
||||
.id = {u"2"_s},
|
||||
.baseName = EmoteName{u"ZeroWidth"_s},
|
||||
},
|
||||
Emote{
|
||||
.name = {u"PogChamp"_s},
|
||||
.id = {u"3"_s},
|
||||
}),
|
||||
.bttv = makeEmotes(
|
||||
Emote{
|
||||
.name = {u"BTTVEmote"_s},
|
||||
},
|
||||
Emote{
|
||||
.name = {u"Kappa"_s},
|
||||
}),
|
||||
.ffz = makeEmotes(
|
||||
Emote{
|
||||
.name = {u"FFZEmote"_s},
|
||||
},
|
||||
Emote{
|
||||
.name = {u"Keepo"_s},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
static MockEmotes twitchdev()
|
||||
{
|
||||
return {
|
||||
.seventv = makeEmotes(Emote{
|
||||
.name = {u"7TVTwitchDev"_s},
|
||||
.id = {u"t5"_s},
|
||||
}),
|
||||
.bttv = makeEmotes(Emote{
|
||||
.name = {u"BTTVTwitchDev"_s},
|
||||
}),
|
||||
.ffz = makeEmotes(Emote{
|
||||
.name = {u"FFZTwitchDev"_s},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
static MockEmotes global()
|
||||
{
|
||||
return {
|
||||
.seventv = makeEmotes(Emote{
|
||||
.name = {u"7TVGlobal"_s},
|
||||
.id = {u"G1"_s},
|
||||
}),
|
||||
.bttv = makeEmotes(Emote{
|
||||
.name = {u"BTTVGlobal"_s},
|
||||
}),
|
||||
.ffz = makeEmotes(Emote{
|
||||
.name = {u"FFZGlobal"_s},
|
||||
}),
|
||||
.twitchAccount = makeEmotes(Emote{
|
||||
.name = {u"MyCoolTwitchEmote"_s},
|
||||
.id = {u"5678"_s},
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const QByteArray CHEERMOTE_JSON{R"({
|
||||
"prefix": "Cheer",
|
||||
"tiers": [
|
||||
{
|
||||
"min_bits": 1,
|
||||
"id": "1",
|
||||
"color": "#979797",
|
||||
"images": {
|
||||
"dark": {
|
||||
"animated": {
|
||||
"1": "https://chatterino.com/bits/1.gif",
|
||||
"2": "https://chatterino.com/bits/2.gif",
|
||||
"4": "https://chatterino.com/bits/4.gif"
|
||||
},
|
||||
"static": {
|
||||
"1": "https://chatterino.com/bits/1.png",
|
||||
"2": "https://chatterino.com/bits/2.png",
|
||||
"4": "https://chatterino.com/bits/4.png"
|
||||
}
|
||||
}
|
||||
},
|
||||
"can_cheer": true,
|
||||
"show_in_bits_card": true
|
||||
},
|
||||
{
|
||||
"min_bits": 100,
|
||||
"id": "100",
|
||||
"color": "#9c3ee8",
|
||||
"images": {
|
||||
"dark": {
|
||||
"animated": {
|
||||
"1": "https://chatterino.com/bits/1.gif",
|
||||
"2": "https://chatterino.com/bits/2.gif",
|
||||
"4": "https://chatterino.com/bits/4.gif"
|
||||
},
|
||||
"static": {
|
||||
"1": "https://chatterino.com/bits/1.png",
|
||||
"2": "https://chatterino.com/bits/2.png",
|
||||
"4": "https://chatterino.com/bits/4.png"
|
||||
}
|
||||
}
|
||||
},
|
||||
"can_cheer": true,
|
||||
"show_in_bits_card": true
|
||||
}
|
||||
],
|
||||
"type": "global_first_party",
|
||||
"order": 1,
|
||||
"last_updated": "2018-05-22T00:06:04Z",
|
||||
"is_charitable": false
|
||||
})"_ba};
|
||||
|
||||
const QByteArray LOCAL_BADGE_JSON{R"({
|
||||
"data": [
|
||||
{
|
||||
"set_id": "subscriber",
|
||||
"versions": [
|
||||
{
|
||||
"click_url": null,
|
||||
"description": "Subscriber",
|
||||
"id": "3072",
|
||||
"image_url_1x": "https://chatterino.com/tb-1",
|
||||
"image_url_2x": "https://chatterino.com/tb-2",
|
||||
"image_url_4x": "https://chatterino.com/tb-3",
|
||||
"title": "Subscriber"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})"_ba};
|
||||
|
||||
const QByteArray SETTINGS_DEFAULT{"{}"_ba};
|
||||
|
||||
std::shared_ptr<TwitchChannel> makeMockTwitchChannel(
|
||||
const QString &name, const testlib::Snapshot &snapshot)
|
||||
{
|
||||
auto chan = std::make_shared<TwitchChannel>(name);
|
||||
auto mocks = MockEmotes::channel();
|
||||
chan->setSeventvEmotes(std::move(mocks.seventv));
|
||||
chan->setBttvEmotes(std::move(mocks.bttv));
|
||||
chan->setFfzEmotes(std::move(mocks.ffz));
|
||||
|
||||
QJsonObject defaultImage{
|
||||
{u"url_1x"_s, u"https://chatterino.com/reward1x.png"_s},
|
||||
{u"url_2x"_s, u"https://chatterino.com/reward2x.png"_s},
|
||||
{u"url_4x"_s, u"https://chatterino.com/reward4x.png"_s},
|
||||
};
|
||||
chan->addKnownChannelPointReward({{
|
||||
{u"channel_id"_s, u"11148817"_s},
|
||||
{u"id"_s, u"unused"_s},
|
||||
{u"reward"_s,
|
||||
{{
|
||||
{u"channel_id"_s, u"11148817"_s},
|
||||
{u"cost"_s, 1},
|
||||
{u"id"_s, u"31a2344e-0fce-4229-9453-fb2e8b6dd02c"_s},
|
||||
{u"is_user_input_required"_s, true},
|
||||
{u"title"_s, u"my reward"_s},
|
||||
{u"image"_s, defaultImage},
|
||||
}}},
|
||||
}});
|
||||
chan->addKnownChannelPointReward({{
|
||||
{u"channel_id"_s, u"11148817"_s},
|
||||
{u"id"_s, u"unused"_s},
|
||||
{u"reward"_s,
|
||||
{{
|
||||
{u"channel_id"_s, u"11148817"_s},
|
||||
{u"cost"_s, 1},
|
||||
{u"id"_s, u"dc8d1dac-256e-42b9-b7ba-40b32e5294e2"_s},
|
||||
{u"is_user_input_required"_s, false},
|
||||
{u"title"_s, u"test"_s},
|
||||
{u"image"_s, defaultImage},
|
||||
}}},
|
||||
}});
|
||||
chan->addKnownChannelPointReward({{
|
||||
{u"channel_id"_s, u"11148817"_s},
|
||||
{u"id"_s, u"unused"_s},
|
||||
{u"reward"_s,
|
||||
{{
|
||||
{u"channel_id"_s, u"11148817"_s},
|
||||
{u"cost"_s, 1},
|
||||
{u"default_bits_cost"_s, 2},
|
||||
{u"bits_cost"_s, 0},
|
||||
{u"pricing_type"_s, u"BITS"_s},
|
||||
{u"reward_type"_s, u"CELEBRATION"_s},
|
||||
{u"is_user_input_required"_s, false},
|
||||
{u"title"_s, u"BitReward"_s},
|
||||
{u"image"_s, defaultImage},
|
||||
}}},
|
||||
{u"redemption_metadata"_s,
|
||||
QJsonObject{
|
||||
{u"celebration_emote_metadata"_s,
|
||||
QJsonObject{
|
||||
{u"emote"_s,
|
||||
{{
|
||||
{u"id"_s, u"42"_s},
|
||||
{u"token"_s, u"MyBitsEmote"_s},
|
||||
}}},
|
||||
}},
|
||||
}},
|
||||
}});
|
||||
|
||||
chan->setUserColor("UserColor", {1, 2, 3, 4});
|
||||
chan->setUserColor("UserColor2", {5, 6, 7, 8});
|
||||
chan->addRecentChatter("UserChatter");
|
||||
chan->addRecentChatter("UserColor");
|
||||
|
||||
chan->setCheerEmoteSets({
|
||||
HelixCheermoteSet{QJsonDocument::fromJson(CHEERMOTE_JSON).object()},
|
||||
});
|
||||
|
||||
chan->setFfzChannelBadges({{u"123456"_s, {3, 4}}});
|
||||
|
||||
chan->addTwitchBadgeSets(HelixChannelBadges{
|
||||
QJsonDocument::fromJson(LOCAL_BADGE_JSON).object(),
|
||||
});
|
||||
|
||||
if (snapshot.param("ffzCustomVipBadge").toBool())
|
||||
{
|
||||
chan->setFfzCustomVipBadge(std::make_shared<Emote>(Emote{
|
||||
.name = {},
|
||||
.images = {Url{"https://chatterino.com/ffz-vip1x.png"}},
|
||||
.tooltip = {"VIP"},
|
||||
.homePage = {},
|
||||
}));
|
||||
}
|
||||
if (snapshot.param("ffzCustomModBadge").toBool())
|
||||
{
|
||||
chan->setFfzCustomModBadge(std::make_shared<Emote>(Emote{
|
||||
.name = {},
|
||||
.images = {Url{"https://chatterino.com/ffz-mod1x.png"}},
|
||||
.tooltip = {"Moderator"},
|
||||
.homePage = {},
|
||||
}));
|
||||
}
|
||||
|
||||
return chan;
|
||||
}
|
||||
|
||||
QT_WARNING_POP
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(MessageBuilder, CommaSeparatedListTagParsing)
|
||||
@@ -408,7 +742,7 @@ TEST_F(TestMessageBuilder, ParseTwitchEmotes)
|
||||
|
||||
for (const auto &test : testCases)
|
||||
{
|
||||
auto *privmsg = static_cast<Communi::IrcPrivateMessage *>(
|
||||
auto *privmsg = dynamic_cast<Communi::IrcPrivateMessage *>(
|
||||
Communi::IrcPrivateMessage::fromData(test.input, nullptr));
|
||||
QString originalMessage = privmsg->content();
|
||||
|
||||
@@ -423,73 +757,6 @@ TEST_F(TestMessageBuilder, ParseTwitchEmotes)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestMessageBuilder, ParseMessage)
|
||||
{
|
||||
MockChannel channel("pajlada");
|
||||
|
||||
struct TestCase {
|
||||
QByteArray input;
|
||||
};
|
||||
|
||||
std::vector<TestCase> testCases{
|
||||
{
|
||||
// action /me 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)",
|
||||
},
|
||||
{
|
||||
R"(@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa)",
|
||||
},
|
||||
{
|
||||
R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=1902:0-4;first-msg=0;flags=;id=9b1c3cb9-7817-47ea-add1-f9d4a9b4f846;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201095690;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Keepo)",
|
||||
},
|
||||
{
|
||||
R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:0-4/1902:6-10/305954156:12-19;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa Keepo PogChamp)",
|
||||
},
|
||||
{
|
||||
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4,6-10;first-msg=0;flags=;id=f7516287-e5d1-43ca-974e-fe0cff84400b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204375009;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa Kappa)",
|
||||
},
|
||||
{
|
||||
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emotes=25:0-4,8-12;first-msg=0;flags=;id=44f85d39-b5fb-475d-8555-f4244f2f7e82;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204423418;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa 😂 Kappa)",
|
||||
},
|
||||
{
|
||||
// start out of range
|
||||
R"(@emotes=84608:9-10 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||
},
|
||||
{
|
||||
// one character emote
|
||||
R"(@emotes=84608:0-0 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||
},
|
||||
{
|
||||
// two character emote
|
||||
R"(@emotes=84609:0-1 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||
},
|
||||
{
|
||||
// end out of range
|
||||
R"(@emotes=84608:0-15 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||
},
|
||||
{
|
||||
// range bad (end character before start)
|
||||
R"(@emotes=84608:15-2 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto &test : testCases)
|
||||
{
|
||||
auto *privmsg = dynamic_cast<Communi::IrcPrivateMessage *>(
|
||||
Communi::IrcPrivateMessage::fromData(test.input, nullptr));
|
||||
EXPECT_NE(privmsg, nullptr);
|
||||
|
||||
QString originalMessage = privmsg->content();
|
||||
|
||||
MessageBuilder builder(&channel, privmsg, MessageParseArgs{});
|
||||
|
||||
auto msg = builder.build();
|
||||
EXPECT_NE(msg.get(), nullptr);
|
||||
|
||||
delete privmsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestMessageBuilder, IgnoresReplace)
|
||||
{
|
||||
struct TestCase {
|
||||
@@ -627,3 +894,172 @@ TEST_F(TestMessageBuilder, IgnoresReplace)
|
||||
<< "' and output '" << message << "'";
|
||||
}
|
||||
}
|
||||
|
||||
class TestMessageBuilderP : public ::testing::TestWithParam<QString>
|
||||
{
|
||||
public:
|
||||
void SetUp() override
|
||||
{
|
||||
auto param = TestMessageBuilderP::GetParam();
|
||||
this->snapshot = testlib::Snapshot::read(IRC_CATEGORY, param);
|
||||
|
||||
this->mockApplication =
|
||||
std::make_unique<MockApplication>(QString::fromUtf8(
|
||||
this->snapshot->mergedSettings(SETTINGS_DEFAULT)));
|
||||
auto mocks = MockEmotes::global();
|
||||
this->mockApplication->seventvEmotes.setGlobalEmotes(mocks.seventv);
|
||||
this->mockApplication->bttvEmotes.setEmotes(mocks.bttv);
|
||||
this->mockApplication->ffzEmotes.setEmotes(mocks.ffz);
|
||||
this->mockApplication->getAccounts()->twitch.getCurrent()->setEmotes(
|
||||
mocks.twitchAccount);
|
||||
this->mockApplication->getUserData()->setUserColor(u"117691339"_s,
|
||||
u"#DAA521"_s);
|
||||
|
||||
this->mockApplication->getAccounts()
|
||||
->twitch.getCurrent()
|
||||
->blockUserLocally(u"12345"_s);
|
||||
|
||||
auto makeBadge = [](QStringView platform) {
|
||||
return std::make_shared<Emote>(Emote{
|
||||
.name = {},
|
||||
.images = {Url{u"https://chatterino.com/" % platform %
|
||||
u".png"}},
|
||||
.tooltip = {platform % u" badge"},
|
||||
.homePage = {},
|
||||
.zeroWidth = false,
|
||||
.id = {},
|
||||
.author = {},
|
||||
.baseName = {},
|
||||
});
|
||||
};
|
||||
|
||||
// Chatterino
|
||||
this->mockApplication->chatterinoBadges.setBadge(
|
||||
{u"123456"_s}, makeBadge(u"Chatterino"));
|
||||
|
||||
// FFZ
|
||||
this->mockApplication->ffzBadges.registerBadge(
|
||||
1, {.emote = makeBadge(u"FFZ1"), .color = {9, 10, 11, 12}});
|
||||
this->mockApplication->ffzBadges.registerBadge(
|
||||
2, {.emote = makeBadge(u"FFZ2"), .color = {13, 14, 15, 16}});
|
||||
this->mockApplication->ffzBadges.registerBadge(
|
||||
3, {.emote = makeBadge(u"FFZ2"), .color = {17, 18, 19, 20}});
|
||||
this->mockApplication->ffzBadges.registerBadge(
|
||||
4, {.emote = makeBadge(u"FFZ2"), .color = {21, 22, 23, 24}});
|
||||
this->mockApplication->getFfzBadges()->assignBadgeToUser({u"123456"_s},
|
||||
1);
|
||||
this->mockApplication->getFfzBadges()->assignBadgeToUser({u"123456"_s},
|
||||
2);
|
||||
|
||||
// 7TV
|
||||
this->mockApplication->getSeventvBadges()->registerBadge({
|
||||
{u"id"_s, u"1"_s},
|
||||
{u"tooltip"_s, u"7TV badge"_s},
|
||||
{
|
||||
u"host"_s,
|
||||
{{
|
||||
{u"url"_s, u"//chatterino.com/7tv/"_s},
|
||||
{u"files"_s,
|
||||
QJsonArray{
|
||||
{{
|
||||
{u"name"_s, u"1x"_s},
|
||||
{u"format"_s, u"WEBP"_s},
|
||||
{u"width"_s, 16},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
});
|
||||
this->mockApplication->getSeventvBadges()->assignBadgeToUser(
|
||||
u"1"_s, {u"123456"_s});
|
||||
|
||||
// Twitch
|
||||
this->mockApplication->getTwitchBadges()->loadLocalBadges();
|
||||
|
||||
this->twitchdevChannel = std::make_shared<TwitchChannel>("twitchdev");
|
||||
this->twitchdevChannel->setRoomId("141981764");
|
||||
|
||||
auto tdMocks = MockEmotes::twitchdev();
|
||||
this->twitchdevChannel->setSeventvEmotes(std::move(tdMocks.seventv));
|
||||
this->twitchdevChannel->setBttvEmotes(std::move(tdMocks.bttv));
|
||||
this->twitchdevChannel->setFfzEmotes(std::move(tdMocks.ffz));
|
||||
|
||||
this->mockApplication->twitch.mockChannels.emplace(
|
||||
"twitchdev", this->twitchdevChannel);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
this->twitchdevChannel.reset();
|
||||
this->mockApplication.reset();
|
||||
this->snapshot.reset();
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchChannel> twitchdevChannel;
|
||||
std::unique_ptr<MockApplication> mockApplication;
|
||||
std::unique_ptr<testlib::Snapshot> snapshot;
|
||||
};
|
||||
|
||||
/// This tests the process of parsing IRC messages and emitting `MessagePtr`s.
|
||||
///
|
||||
/// Even though it's in the message builder category, this uses
|
||||
/// `IrcMesssageHandler` to ensure the correct (or: "real") arguments to build
|
||||
/// messages.
|
||||
///
|
||||
/// Tests are contained in `tests/snapshots/MessageBuilder/IRC`. Fixtures
|
||||
/// consist of an object with the keys `input`, `output`, `settings` (optional),
|
||||
/// and `params` (optional).
|
||||
///
|
||||
/// `UPDATE_SNAPSHOTS` (top) controls whether the `output` will be generated or
|
||||
/// checked.
|
||||
///
|
||||
/// `params` is an optional object with the following keys:
|
||||
/// - `prevMessages`: An array of past messages (used for replies)
|
||||
/// - `findAllUsernames`: A boolean controlling the equally named setting
|
||||
/// (default: false)
|
||||
TEST_P(TestMessageBuilderP, Run)
|
||||
{
|
||||
auto channel = makeMockTwitchChannel(u"pajlada"_s, *snapshot);
|
||||
|
||||
std::vector<MessagePtr> prevMessages;
|
||||
|
||||
for (auto prevInput : snapshot->param("prevMessages").toArray())
|
||||
{
|
||||
auto *ircMessage = Communi::IrcMessage::fromData(
|
||||
prevInput.toString().toUtf8(), nullptr);
|
||||
ASSERT_NE(ircMessage, nullptr);
|
||||
auto builtMessages = IrcMessageHandler::parseMessageWithReply(
|
||||
channel.get(), ircMessage, prevMessages);
|
||||
for (const auto &builtMessage : builtMessages)
|
||||
{
|
||||
prevMessages.emplace_back(builtMessage);
|
||||
}
|
||||
delete ircMessage;
|
||||
}
|
||||
|
||||
auto *ircMessage =
|
||||
Communi::IrcMessage::fromData(snapshot->inputUtf8(), nullptr);
|
||||
ASSERT_NE(ircMessage, nullptr);
|
||||
|
||||
auto builtMessages = IrcMessageHandler::parseMessageWithReply(
|
||||
channel.get(), ircMessage, prevMessages);
|
||||
|
||||
QJsonArray got;
|
||||
for (const auto &msg : builtMessages)
|
||||
{
|
||||
got.append(msg->toJson());
|
||||
}
|
||||
|
||||
delete ircMessage;
|
||||
|
||||
ASSERT_TRUE(snapshot->run(got, UPDATE_SNAPSHOTS));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
IrcMessage, TestMessageBuilderP,
|
||||
testing::ValuesIn(testlib::Snapshot::discover(IRC_CATEGORY)));
|
||||
|
||||
TEST(TestMessageBuilderP, Integrity)
|
||||
{
|
||||
ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "lib/Snapshot.hpp"
|
||||
|
||||
#include "common/Literals.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QStringBuilder>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino::literals;
|
||||
|
||||
bool compareJson(const QJsonValue &expected, const QJsonValue &got,
|
||||
const QString &context)
|
||||
{
|
||||
if (expected == got)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (expected.type() != got.type())
|
||||
{
|
||||
qWarning() << context
|
||||
<< "- mismatching type - expected:" << expected.type()
|
||||
<< "got:" << got.type();
|
||||
return false;
|
||||
}
|
||||
switch (expected.type())
|
||||
{
|
||||
case QJsonValue::Array: {
|
||||
auto expArr = expected.toArray();
|
||||
auto gotArr = got.toArray();
|
||||
if (expArr.size() != gotArr.size())
|
||||
{
|
||||
qWarning() << context << "- Mismatching array size - expected:"
|
||||
<< expArr.size() << "got:" << gotArr.size();
|
||||
return false;
|
||||
}
|
||||
for (QJsonArray::size_type i = 0; i < expArr.size(); i++)
|
||||
{
|
||||
if (!compareJson(expArr[i], gotArr[i],
|
||||
context % '[' % QString::number(i) % ']'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // unreachable
|
||||
case QJsonValue::Object: {
|
||||
auto expObj = expected.toObject();
|
||||
auto gotObj = got.toObject();
|
||||
if (expObj.size() != gotObj.size())
|
||||
{
|
||||
qWarning() << context << "- Mismatching object size - expected:"
|
||||
<< expObj.size() << "got:" << gotObj.size();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto it = expObj.constBegin(); it != expObj.constEnd(); it++)
|
||||
{
|
||||
if (!gotObj.contains(it.key()))
|
||||
{
|
||||
qWarning() << context << "- Object doesn't contain key"
|
||||
<< it.key();
|
||||
return false;
|
||||
}
|
||||
if (!compareJson(it.value(), gotObj[it.key()],
|
||||
context % '.' % it.key()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case QJsonValue::Null:
|
||||
case QJsonValue::Bool:
|
||||
case QJsonValue::Double:
|
||||
case QJsonValue::String:
|
||||
case QJsonValue::Undefined:
|
||||
break;
|
||||
}
|
||||
|
||||
qWarning() << context << "- expected:" << expected << "got:" << got;
|
||||
return false;
|
||||
}
|
||||
|
||||
void mergeJson(QJsonObject &base, const QJsonObject &additional)
|
||||
{
|
||||
for (auto it = additional.begin(); it != additional.end(); it++)
|
||||
{
|
||||
auto ref = base[it.key()];
|
||||
|
||||
if (ref.isArray())
|
||||
{
|
||||
// there's no way of pushing to the array without detaching first
|
||||
auto arr = ref.toArray();
|
||||
if (!it->isArray())
|
||||
{
|
||||
throw std::runtime_error("Mismatched types");
|
||||
}
|
||||
|
||||
// append all additional values
|
||||
auto addArr = it->toArray();
|
||||
for (auto v : addArr)
|
||||
{
|
||||
arr.append(v);
|
||||
}
|
||||
ref = arr;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ref.isObject())
|
||||
{
|
||||
// same here, detach first and overwrite
|
||||
auto obj = ref.toObject();
|
||||
if (!it->isObject())
|
||||
{
|
||||
throw std::runtime_error("Mismatched types");
|
||||
}
|
||||
mergeJson(obj, it->toObject());
|
||||
ref = obj;
|
||||
continue;
|
||||
}
|
||||
|
||||
ref = it.value(); // overwrite for simple types/non-existent keys
|
||||
}
|
||||
}
|
||||
|
||||
QDir baseDir(const QString &category)
|
||||
{
|
||||
QDir snapshotDir(QStringLiteral(__FILE__));
|
||||
snapshotDir.cd("../../../snapshots/");
|
||||
snapshotDir.cd(category);
|
||||
return snapshotDir;
|
||||
}
|
||||
|
||||
QString filePath(const QString &category, const QString &name)
|
||||
{
|
||||
return baseDir(category).filePath(name);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino::testlib {
|
||||
|
||||
std::unique_ptr<Snapshot> Snapshot::read(QString category, QString name)
|
||||
{
|
||||
if (!name.endsWith(u".json"))
|
||||
{
|
||||
name.append(u".json");
|
||||
}
|
||||
|
||||
QFile file(filePath(category, name));
|
||||
if (!file.open(QFile::ReadOnly))
|
||||
{
|
||||
throw std::runtime_error("Failed to open file");
|
||||
}
|
||||
auto content = file.readAll();
|
||||
file.close();
|
||||
const auto doc = QJsonDocument::fromJson(content).object();
|
||||
|
||||
return std::unique_ptr<Snapshot>(
|
||||
new Snapshot(std::move(category), std::move(name), doc));
|
||||
}
|
||||
|
||||
QStringList Snapshot::discover(const QString &category)
|
||||
{
|
||||
auto files =
|
||||
baseDir(category).entryList(QDir::NoDotAndDotDot | QDir::Files);
|
||||
for (auto &file : files)
|
||||
{
|
||||
file.remove(".json");
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
bool Snapshot::run(const QJsonValue &got, bool updateSnapshots) const
|
||||
{
|
||||
if (updateSnapshots)
|
||||
{
|
||||
this->write(got);
|
||||
return true;
|
||||
}
|
||||
|
||||
return compareJson(this->output_, got, QStringLiteral("output"));
|
||||
}
|
||||
|
||||
Snapshot::Snapshot(QString category, QString name, const QJsonObject &root)
|
||||
: category_(std::move(category))
|
||||
, name_(std::move(name))
|
||||
, input_(root["input"_L1])
|
||||
, params_(root["params"_L1].toObject())
|
||||
, settings_(root["settings"_L1].toObject())
|
||||
, output_(root["output"_L1])
|
||||
{
|
||||
}
|
||||
|
||||
void Snapshot::write(const QJsonValue &got) const
|
||||
{
|
||||
QFile file(filePath(this->category_, this->name_));
|
||||
if (!file.open(QFile::WriteOnly))
|
||||
{
|
||||
throw std::runtime_error("Failed to open file");
|
||||
}
|
||||
|
||||
QJsonObject obj{
|
||||
{"input"_L1, this->input_},
|
||||
{"output"_L1, got},
|
||||
};
|
||||
if (!this->params_.isEmpty())
|
||||
{
|
||||
obj.insert("params"_L1, this->params_);
|
||||
}
|
||||
if (!this->settings_.isEmpty())
|
||||
{
|
||||
obj.insert("settings"_L1, this->settings_);
|
||||
}
|
||||
|
||||
file.write(QJsonDocument{obj}.toJson());
|
||||
file.close();
|
||||
}
|
||||
|
||||
QByteArray Snapshot::mergedSettings(const QByteArray &base) const
|
||||
{
|
||||
auto baseDoc = QJsonDocument::fromJson(base);
|
||||
if (!baseDoc.isObject())
|
||||
{
|
||||
throw std::runtime_error("Invalid base settings");
|
||||
}
|
||||
auto baseObj = baseDoc.object();
|
||||
mergeJson(baseObj, this->settings_);
|
||||
|
||||
baseDoc.setObject(baseObj);
|
||||
return baseDoc.toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
} // namespace chatterino::testlib
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino::testlib {
|
||||
|
||||
/// @brief JSON based snapshot/approval tests
|
||||
///
|
||||
/// Snapshot tests record the output of some computation based on some @a input.
|
||||
/// Additionally, users can provide @a params. There isn't any rule on what goes
|
||||
/// into @a input vs. @a params - a rule of thumb is to put everything that's
|
||||
/// not directly an input to the target function into @a params (like settings).
|
||||
/// Similarly, settings can be specified in "settings". These can be merged with
|
||||
/// existing settings (the base) in mergedSettings().
|
||||
///
|
||||
/// Snapshots are stored in `tests/snapshots/{category}/{name}.json`.
|
||||
/// `category` can consist of multiple directories (e.g. `foo/bar`).
|
||||
///
|
||||
/// Note that when using CTest, added snapshots are only discovered when
|
||||
/// reloading the tests.
|
||||
///
|
||||
/// @par A minimal example
|
||||
///
|
||||
/// ```cpp
|
||||
/// #include "lib/Snapshot.hpp"
|
||||
/// #include "Test.hpp"
|
||||
///
|
||||
/// #include <array>
|
||||
/// #include <QStringBuilder>
|
||||
///
|
||||
/// namespace testlib = chatterino::testlib;
|
||||
///
|
||||
/// constexpr bool UPDATE_SNAPSHOTS = false;
|
||||
///
|
||||
/// class ExampleTest : public ::testing::TestWithParam<QString> {};
|
||||
///
|
||||
/// TEST_P(ExampleTest, Run) {
|
||||
/// auto fixture = testlib::Snapshot::read("category", GetParam());
|
||||
/// auto output = functionToTest(fixture.input()); // or input{String,Utf8}
|
||||
/// // if snapshots are supposed to be updated, this will write the output
|
||||
/// ASSERT_TRUE(fixture.run(output, UPDATE_SNAPSHOTS));
|
||||
/// }
|
||||
///
|
||||
/// INSTANTIATE_TEST_SUITE_P(ExampleInstance, ExampleTest,
|
||||
/// testing::ValuesIn(testlib::Snapshot::discover("category")));
|
||||
///
|
||||
/// // verify that all snapshots are included
|
||||
/// TEST(ExampleTest, Integrity) {
|
||||
/// ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested
|
||||
/// }
|
||||
/// ```
|
||||
class Snapshot
|
||||
{
|
||||
public:
|
||||
Snapshot(const Snapshot &) = delete;
|
||||
Snapshot &operator=(const Snapshot &) = delete;
|
||||
|
||||
Snapshot(Snapshot &&) = default;
|
||||
Snapshot &operator=(Snapshot &&) = default;
|
||||
~Snapshot() = default;
|
||||
|
||||
/// Read a snapshot
|
||||
static std::unique_ptr<Snapshot> read(QString category, QString name);
|
||||
|
||||
/// Finds all tests in @a category
|
||||
static QStringList discover(const QString &category);
|
||||
|
||||
/// @brief Runs the snapshot test
|
||||
///
|
||||
/// If @a updateSnapshots is `false`, this checks that @a got matches the
|
||||
/// expected output (#output()).
|
||||
/// If @a updateSnapshots is `true`, this sets @a got as the expected
|
||||
/// output of this snapshot.
|
||||
bool run(const QJsonValue &got, bool updateSnapshots) const;
|
||||
|
||||
QString name() const
|
||||
{
|
||||
return this->name_;
|
||||
}
|
||||
|
||||
QString category() const
|
||||
{
|
||||
return this->category_;
|
||||
}
|
||||
|
||||
QJsonValue input() const
|
||||
{
|
||||
return this->input_;
|
||||
}
|
||||
|
||||
QString inputString() const
|
||||
{
|
||||
return this->input_.toString();
|
||||
}
|
||||
|
||||
QByteArray inputUtf8() const
|
||||
{
|
||||
return this->input_.toString().toUtf8();
|
||||
}
|
||||
|
||||
QJsonValue param(QLatin1String name) const
|
||||
{
|
||||
return this->params_[name];
|
||||
}
|
||||
QJsonValue param(const char *name) const
|
||||
{
|
||||
return this->param(QLatin1String{name});
|
||||
}
|
||||
|
||||
QByteArray mergedSettings(const QByteArray &base) const;
|
||||
|
||||
QJsonValue output() const
|
||||
{
|
||||
return this->output_;
|
||||
}
|
||||
|
||||
private:
|
||||
Snapshot(QString category, QString name, const QJsonObject &root);
|
||||
|
||||
void write(const QJsonValue &got) const;
|
||||
|
||||
QString category_;
|
||||
QString name_;
|
||||
QJsonValue input_;
|
||||
QJsonObject params_;
|
||||
QJsonObject settings_;
|
||||
QJsonValue output_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::testlib
|
||||
Reference in New Issue
Block a user