feat: add snapshot tests for eventsub messages (#5965)

This commit is contained in:
nerix
2025-02-21 21:33:29 +01:00
committed by GitHub
parent bb4c6d6f6e
commit a86df7087d
17 changed files with 885 additions and 21 deletions
+1
View File
@@ -46,6 +46,7 @@
- Dev: Updated `googletest` to 1.16.0. (#5942)
- Dev: Fixed duplicate CMake configure in clean builds. (#5940)
- Dev: BTTV emotes are now loaded as WEBP. (#5957)
- Dev: Added snapshot tests for EventSub. (#5965)
## 2.5.2
+1 -1
View File
@@ -20,7 +20,7 @@ boost::json::result_for<std::chrono::system_clock::time_point,
std::chrono::system_clock::time_point tp;
std::istringstream in{*raw};
in >> date::parse("%FT%TZ", tp);
in >> date::parse("%FT%H:%M:%12SZ", tp);
return tp;
}
+14 -2
View File
@@ -9,6 +9,7 @@
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "util/Twitch.hpp"
#include <unordered_map>
@@ -45,8 +46,19 @@ public:
ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override
{
assert(false && "unimplemented getChannelOrEmpty in mock irc server");
return {};
QString query = cleanChannelName(dirtyChannelName);
auto it = this->mockChannels.find(query);
if (it == this->mockChannels.end())
{
return Channel::getEmpty();
}
auto chan = it->second.lock();
if (!chan)
{
return Channel::getEmpty();
}
return chan;
}
void addFakeMessage(const QString &data) override
+2
View File
@@ -27,6 +27,7 @@
#include <unordered_map>
class TestIrcMessageHandlerP;
class TestEventSubMessagesP;
namespace chatterino {
@@ -514,6 +515,7 @@ private:
friend class IrcMessageHandler;
friend class Commands_E2E_Test;
friend class ::TestIrcMessageHandlerP;
friend class ::TestEventSubMessagesP;
};
} // namespace chatterino
+3 -14
View File
@@ -27,6 +27,7 @@
#include "singletons/WindowManager.hpp"
#include "util/PostToThread.hpp"
#include "util/RatelimitBucket.hpp"
#include "util/Twitch.hpp"
#include <IrcCommand>
#include <IrcMessage>
@@ -1201,18 +1202,6 @@ std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
return Channel::getEmpty();
}
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
if (dirtyChannelName.startsWith('#'))
{
return dirtyChannelName.mid(1).toLower();
}
else
{
return dirtyChannelName.toLower();
}
}
bool TwitchIrcServer::prepareToSend(
const std::shared_ptr<TwitchChannel> &channel)
{
@@ -1544,7 +1533,7 @@ void TwitchIrcServer::sendRawMessage(const QString &rawMessage)
ChannelPtr TwitchIrcServer::getOrAddChannel(const QString &dirtyChannelName)
{
auto channelName = this->cleanChannelName(dirtyChannelName);
auto channelName = cleanChannelName(dirtyChannelName);
// try get channel
ChannelPtr chan = this->getChannelOrEmpty(channelName);
@@ -1594,7 +1583,7 @@ ChannelPtr TwitchIrcServer::getOrAddChannel(const QString &dirtyChannelName)
ChannelPtr TwitchIrcServer::getChannelOrEmpty(const QString &dirtyChannelName)
{
auto channelName = this->cleanChannelName(dirtyChannelName);
auto channelName = cleanChannelName(dirtyChannelName);
std::lock_guard<std::mutex> lock(this->channelMutex);
-2
View File
@@ -169,8 +169,6 @@ protected:
std::shared_ptr<Channel> getCustomChannel(const QString &channelname);
QString cleanChannelName(const QString &dirtyChannelName);
private:
void onMessageSendRequested(const std::shared_ptr<TwitchChannel> &channel,
const QString &message, bool &sent);
+13 -2
View File
@@ -55,7 +55,10 @@ void Connection::onChannelBan(
BanAction action{};
action.timestamp = std::chrono::steady_clock::now();
if (!getApp()->isTest())
{
action.timestamp = std::chrono::steady_clock::now();
}
action.roomID = roomID;
action.source = ActionUser{
.id = QString::fromStdString(payload.event.moderatorUserID),
@@ -85,6 +88,10 @@ void Connection::onChannelBan(
runInGuiThread([action{std::move(action)}, chan{std::move(chan)}] {
auto time = QDateTime::currentDateTime();
if (getApp()->isTest())
{
time = QDateTime::fromSecsSinceEpoch(0).toUTC();
}
MessageBuilder msg(action, time);
msg->flags.set(MessageFlag::PubSub);
chan->addOrReplaceTimeout(msg.release(), QDateTime::currentDateTime());
@@ -162,7 +169,11 @@ void Connection::onChannelModerate(
return;
}
const auto now = QDateTime::currentDateTime();
auto now = QDateTime::currentDateTime();
if (getApp()->isTest())
{
now = QDateTime::fromSecsSinceEpoch(0).toUTC();
}
std::visit(
[&](auto &&action) {
+10
View File
@@ -62,6 +62,16 @@ void stripChannelName(QString &channelName)
}
}
QString cleanChannelName(const QString &dirtyChannelName)
{
if (dirtyChannelName.startsWith('#'))
{
return dirtyChannelName.mid(1).toLower();
}
return dirtyChannelName.toLower();
}
std::pair<ParsedUserName, ParsedUserID> parseUserNameOrID(const QString &input)
{
if (input.startsWith("id:"))
+3
View File
@@ -16,6 +16,9 @@ void stripUserName(QString &userName);
// stripChannelName removes any @ prefix or , suffix to make it more suitable for command use
void stripChannelName(QString &channelName);
/// Strips a leading `#` and lowercases the name
QString cleanChannelName(const QString &dirtyChannelName);
using ParsedUserName = QString;
using ParsedUserID = QString;
+2
View File
@@ -53,6 +53,8 @@ set(test_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp
${CMAKE_CURRENT_LIST_DIR}/src/OnceFlag.cpp
${CMAKE_CURRENT_LIST_DIR}/src/IncognitoBrowser.cpp
${CMAKE_CURRENT_LIST_DIR}/src/EventSubMessages.cpp
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp
# Add your new file above this line!
@@ -0,0 +1,131 @@
{
"input": {
"banned_at": "2023-05-20T12:30:55.518375571Z",
"broadcaster_user_id": "11148817",
"broadcaster_user_login": "pajlada",
"broadcaster_user_name": "pajlada",
"ends_at": null,
"is_permanent": true,
"moderator_user_id": "29024944",
"moderator_user_login": "CLIModerator",
"moderator_user_name": "CLIModerator",
"reason": "This is a test event",
"user_id": "40389552",
"user_login": "testFromUser",
"user_name": "testFromUser"
},
"output": [
{
"badgeInfos": {
},
"badges": [
],
"channelName": "",
"count": 1,
"displayName": "",
"elements": [
{
"element": {
"color": "System",
"flags": "Timestamp",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"0:00"
]
},
"flags": "Timestamp",
"format": "",
"link": {
"type": "None",
"value": ""
},
"time": "00:00:00",
"tooltip": "",
"trailingSpace": true,
"type": "TimestampElement"
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "UserInfo",
"value": "CLIModerator"
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"CLIModerator"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"banned"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "UserInfo",
"value": "testFromUser"
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"testFromUser:"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"\"This",
"is",
"a",
"test",
"event\"."
]
}
],
"flags": "System|Timeout|PubSub",
"id": "",
"localizedName": "",
"loginName": "CLIModerator",
"messageText": "CLIModerator banned testFromUser: \"This is a test event\". ",
"searchText": "CLIModerator banned testFromUser: \"This is a test event\". ",
"serverReceivedTime": "1970-01-01T00:00:00Z",
"timeoutUser": "testFromUser",
"userID": "",
"usernameColor": "#ff000000"
}
]
}
@@ -0,0 +1,134 @@
{
"input": {
"banned_at": "2023-05-20T12:30:55.518375571Z",
"broadcaster_user_id": "11148817",
"broadcaster_user_login": "pajlada",
"broadcaster_user_name": "pajlada",
"ends_at": "2023-05-20T12:40:55.518375571Z",
"is_permanent": false,
"moderator_user_id": "29024944",
"moderator_user_login": "CLIModerator",
"moderator_user_name": "CLIModerator",
"reason": "This is a test event",
"user_id": "40389552",
"user_login": "testFromUser",
"user_name": "testFromUser"
},
"output": [
{
"badgeInfos": {
},
"badges": [
],
"channelName": "",
"count": 1,
"displayName": "",
"elements": [
{
"element": {
"color": "System",
"flags": "Timestamp",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"0:00"
]
},
"flags": "Timestamp",
"format": "",
"link": {
"type": "None",
"value": ""
},
"time": "00:00:00",
"tooltip": "",
"trailingSpace": true,
"type": "TimestampElement"
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "UserInfo",
"value": "CLIModerator"
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"CLIModerator"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"timed",
"out"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "UserInfo",
"value": "testFromUser"
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"testFromUser"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"for",
"10m:",
"\"This",
"is",
"a",
"test",
"event\"."
]
}
],
"flags": "System|Timeout|PubSub",
"id": "",
"localizedName": "",
"loginName": "CLIModerator",
"messageText": "CLIModerator timed out testFromUser for 10m: \"This is a test event\". ",
"searchText": "CLIModerator timed out testFromUser for 10m: \"This is a test event\". ",
"serverReceivedTime": "1970-01-01T00:00:00Z",
"timeoutUser": "testFromUser",
"userID": "",
"usernameColor": "#ff000000"
}
]
}
@@ -0,0 +1,174 @@
{
"input": {
"action": "warn",
"automod_terms": null,
"ban": null,
"broadcaster_user_id": "11148817",
"broadcaster_user_login": "pajlada",
"broadcaster_user_name": "pajlada",
"delete": null,
"followers": null,
"moderator_user_id": "424596340",
"moderator_user_login": "quotrok",
"moderator_user_name": "quotrok",
"raid": null,
"shared_chat_ban": null,
"shared_chat_delete": null,
"shared_chat_timeout": null,
"shared_chat_unban": null,
"shared_chat_untimeout": null,
"slow": null,
"source_broadcaster_user_id": "41292030",
"source_broadcaster_user_login": "adflynn404",
"source_broadcaster_user_name": "adflynn404",
"timeout": null,
"unban": null,
"unban_request": null,
"unmod": null,
"unraid": null,
"untimeout": null,
"unvip": null,
"vip": null,
"warn": {
"chat_rules_cited": null,
"reason": "cut it out",
"user_id": "141981764",
"user_login": "twitchdev",
"user_name": "TwitchDev"
}
},
"output": [
{
"badgeInfos": {
},
"badges": [
],
"channelName": "",
"count": 1,
"displayName": "",
"elements": [
{
"element": {
"color": "System",
"flags": "Timestamp",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"0:00"
]
},
"flags": "Timestamp",
"format": "",
"link": {
"type": "None",
"value": ""
},
"time": "00:00:00",
"tooltip": "",
"trailingSpace": true,
"type": "TimestampElement"
},
{
"color": "Text",
"fallbackColor": "System",
"flags": "Text|Mention",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "MentionElement",
"userColor": "System",
"userLoginName": "quotrok",
"words": [
"quotrok"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"has",
"warned"
]
},
{
"color": "Text",
"fallbackColor": "System",
"flags": "Text|Mention",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": false,
"type": "MentionElement",
"userColor": "System",
"userLoginName": "twitchdev",
"words": [
"TwitchDev"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
":"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"cut",
"it",
"out"
]
}
],
"flags": "System|Timeout",
"id": "",
"localizedName": "",
"loginName": "quotrok",
"messageText": "quotrok has warned twitchdev: cut it out ",
"searchText": "quotrok has warned twitchdev: cut it out ",
"serverReceivedTime": "1970-01-01T00:00:00Z",
"timeoutUser": "",
"userID": "",
"usernameColor": "#ff000000"
}
]
}
@@ -0,0 +1,174 @@
{
"input": {
"action": "warn",
"automod_terms": null,
"ban": null,
"broadcaster_user_id": "11148817",
"broadcaster_user_login": "pajlada",
"broadcaster_user_name": "pajlada",
"delete": null,
"followers": null,
"moderator_user_id": "424596340",
"moderator_user_login": "quotrok",
"moderator_user_name": "quotrok",
"raid": null,
"shared_chat_ban": null,
"shared_chat_delete": null,
"shared_chat_timeout": null,
"shared_chat_unban": null,
"shared_chat_untimeout": null,
"slow": null,
"source_broadcaster_user_id": "11148817",
"source_broadcaster_user_login": "pajlada",
"source_broadcaster_user_name": "pajlada",
"timeout": null,
"unban": null,
"unban_request": null,
"unmod": null,
"unraid": null,
"untimeout": null,
"unvip": null,
"vip": null,
"warn": {
"chat_rules_cited": null,
"reason": "cut it out",
"user_id": "141981764",
"user_login": "twitchdev",
"user_name": "TwitchDev"
}
},
"output": [
{
"badgeInfos": {
},
"badges": [
],
"channelName": "",
"count": 1,
"displayName": "",
"elements": [
{
"element": {
"color": "System",
"flags": "Timestamp",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"0:00"
]
},
"flags": "Timestamp",
"format": "",
"link": {
"type": "None",
"value": ""
},
"time": "00:00:00",
"tooltip": "",
"trailingSpace": true,
"type": "TimestampElement"
},
{
"color": "Text",
"fallbackColor": "System",
"flags": "Text|Mention",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "MentionElement",
"userColor": "System",
"userLoginName": "quotrok",
"words": [
"quotrok"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"has",
"warned"
]
},
{
"color": "Text",
"fallbackColor": "System",
"flags": "Text|Mention",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": false,
"type": "MentionElement",
"userColor": "System",
"userLoginName": "twitchdev",
"words": [
"TwitchDev"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
":"
]
},
{
"color": "System",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"cut",
"it",
"out"
]
}
],
"flags": "System|Timeout",
"id": "",
"localizedName": "",
"loginName": "quotrok",
"messageText": "quotrok has warned twitchdev: cut it out ",
"searchText": "quotrok has warned twitchdev: cut it out ",
"serverReceivedTime": "1970-01-01T00:00:00Z",
"timeoutUser": "",
"userID": "",
"usernameColor": "#ff000000"
}
]
}
+197
View File
@@ -0,0 +1,197 @@
#include "common/Literals.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "lib/Snapshot.hpp"
#include "messages/Message.hpp"
#include "mocks/BaseApplication.hpp"
#include "mocks/Logging.hpp"
#include "mocks/TwitchIrcServer.hpp"
#include "providers/twitch/eventsub/Connection.hpp"
#include "Test.hpp"
#include "util/QCompareCaseInsensitive.hpp"
#include <QString>
using namespace chatterino;
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 CATEGORY = u"EventSub"_s;
/// JSON for the `subscription` object in `payload`
const std::map<QString, std::string_view, QCompareCaseInsensitive>
SUBSCRIPTIONS{
{
"channel-moderate",
R"({
"id": "7297f7eb-3bf5-461f-8ae6-7cd7781ebce3",
"status": "enabled",
"type": "channel.moderate",
"version": "2",
"cost": 0,
"condition": {
"broadcaster_user_id": "11148817",
"moderator_user_id": "11148817"
},
"transport": {
"method": "websocket",
"session_id": "AgoQUlB8aB2SSsavWVfcs5ljnBIGY2VsbC1j"
},
"created_at": "2024-02-23T21:12:33.771005262Z"
})",
},
{
"channel-ban",
R"({
"id": "4aa632e0-fca3-590b-e981-bbd12abdb3fe",
"status": "enabled",
"type": "channel.ban",
"version": "1",
"condition": { "broadcaster_user_id": "11148817" },
"transport": { "method": "websocket", "session_id": "38de428e_b11f07be" },
"created_at": "2023-05-20T12:30:55.518375571Z",
"cost": 0
})",
},
};
class MockApplication : public mock::BaseApplication
{
public:
MockApplication() = default;
ILogging *getChatLogger() override
{
return &this->logging;
}
ITwitchIrcServer *getTwitch() override
{
return &this->twitch;
}
AccountController *getAccounts() override
{
return &this->accounts;
}
mock::EmptyLogging logging;
mock::MockTwitchIrcServer twitch;
AccountController accounts;
};
std::shared_ptr<TwitchChannel> makeMockTwitchChannel(const QString &name)
{
auto chan = std::make_shared<TwitchChannel>(name);
return chan;
}
boost::beast::flat_buffer makePayload(std::string_view subJson,
const QJsonObject &event)
{
auto subscription =
QJsonDocument::fromJson(
QByteArray::fromRawData(subJson.data(),
static_cast<qsizetype>(subJson.size())))
.object();
QJsonObject metadata{
{"message_id", "e8edc592-5550-4aa5-bba6-39e31a2be435"},
{"message_type", "notification"},
{"message_timestamp", "2024-05-14T12:31:47.995298776Z"},
{"subscription_type", subscription["type"]},
{"subscription_version", subscription["version"]},
};
QJsonObject payload{
{"metadata", metadata},
{"payload",
QJsonObject{
{"subscription", subscription},
{"event", event},
}},
};
auto bytes = QJsonDocument(payload).toJson();
boost::beast::flat_buffer buf;
auto inner = buf.prepare(bytes.size());
std::memcpy(inner.data(), bytes.data(), inner.size());
buf.commit(inner.size());
return buf;
}
} // namespace
class TestEventSubMessagesP : public ::testing::TestWithParam<QString>
{
public:
void SetUp() override
{
auto param = TestEventSubMessagesP::GetParam();
this->snapshot = testlib::Snapshot::read(CATEGORY, param);
this->mockApplication = std::make_unique<MockApplication>();
this->mainChannel = makeMockTwitchChannel("pajlada");
this->mainChannel->setRoomId("11148817");
this->mockApplication->twitch.mockChannels.emplace("pajlada",
this->mainChannel);
}
void TearDown() override
{
this->mainChannel.reset();
this->mockApplication.reset();
this->snapshot.reset();
}
std::shared_ptr<TwitchChannel> mainChannel;
std::unique_ptr<MockApplication> mockApplication;
std::unique_ptr<testlib::Snapshot> snapshot;
};
TEST_P(TestEventSubMessagesP, Run)
{
QStringView subcategory(snapshot->name());
subcategory = subcategory.sliced(0, subcategory.indexOf('/'));
auto subscription = SUBSCRIPTIONS.find(subcategory);
ASSERT_NE(subscription, SUBSCRIPTIONS.end()) << subcategory;
auto json = makePayload(subscription->second, snapshot->input().toObject());
std::unique_ptr<eventsub::lib::Listener> listener =
std::make_unique<eventsub::Connection>();
auto ec = eventsub::lib::handleMessage(listener, json);
ASSERT_FALSE(ec.failed())
<< ec.what() << ec.message() << ec.location().to_string();
auto messages = mainChannel->getMessageSnapshot();
QJsonArray output;
for (const auto &message : messages)
{
output.append(message->toJson());
}
ASSERT_TRUE(snapshot->run(output, UPDATE_SNAPSHOTS))
<< "Snapshot " << snapshot->name() << " failed. Expected JSON to be\n"
<< QJsonDocument(snapshot->output().toArray()).toJson() << "\nbut got\n"
<< QJsonDocument(output).toJson() << "\ninstead.";
}
INSTANTIATE_TEST_SUITE_P(
EventSubMessages, TestEventSubMessagesP,
testing::ValuesIn(testlib::Snapshot::discoverNested(CATEGORY)));
TEST(TestEventSubMessagesP, Integrity)
{
ASSERT_FALSE(UPDATE_SNAPSHOTS); // make sure fixtures are actually tested
}
+23
View File
@@ -175,6 +175,29 @@ QStringList Snapshot::discover(const QString &category)
return files;
}
QStringList Snapshot::discoverNested(const QString &category)
{
auto directories =
baseDir(category).entryList(QDir::NoDotAndDotDot | QDir::Dirs);
QStringList all;
for (const auto &dir : directories)
{
auto d = baseDir(category);
d.cd(dir);
auto files = d.entryList(QDir::NoDotAndDotDot | QDir::Files);
for (const auto &file : files)
{
QStringView view(file);
if (view.endsWith(u".json"))
{
view = view.sliced(0, view.size() - 5);
}
all.append(dir % u'/' % view);
}
}
return all;
}
bool Snapshot::run(const QJsonValue &got, bool updateSnapshots) const
{
if (updateSnapshots)
+3
View File
@@ -70,6 +70,9 @@ public:
/// Finds all tests in @a category
static QStringList discover(const QString &category);
/// Finds all 1-level nested tests in @a category (category/foo/bar)
static QStringList discoverNested(const QString &category);
/// @brief Runs the snapshot test
///
/// If @a updateSnapshots is `false`, this checks that @a got matches the