diff --git a/CHANGELOG.md b/CHANGELOG.md index 552a03adab6..9fe6aa1c791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unversioned +## 2.3.3 + - Major: Added username autocompletion popup menu when typing usernames with an @ prefix. (#1979, #2866) - Major: Added ability to toggle visibility of Channel Tabs - This can be done by right-clicking the tab area or pressing the keyboard shortcut (default: Ctrl+U). (#2600) - Minor: The /live split now shows channels going offline. (#2880) @@ -20,8 +22,9 @@ - Bugfix: Moderation buttons now show the correct time unit when using units other than seconds. (#1719, #2864) - Bugfix: Fixed FFZ emote links for global emotes (#2807, #2808) - Bugfix: Fixed pasting text with URLs included (#1688, #2855) -- Bugfix: Fix reconnecting when IRC write connection is lost (#1831, #2356, #2850) +- Bugfix: Fix reconnecting when IRC write connection is lost (#1831, #2356, #2850, #2892) - Bugfix: Fixed bit and new subscriber emotes not (re)loading in some rare cases. (#2856, #2857) +- Bugfix: Fixed subscription emotes showing up incorrectly in the emote menu. (#2905) ## 2.3.2 diff --git a/CMakeLists.txt b/CMakeLists.txt index ef5c2d31281..22fab10c3e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake" ) -project(chatterino VERSION 2.3.2) +project(chatterino VERSION 2.3.3) option(BUILD_APP "Build Chatterino" ON) option(BUILD_TESTS "Build the tests for Chatterino" OFF) diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 9300ab32b86..a0802b1633b 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -32,6 +32,6 @@ chatterino - + diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 1ee40da0d1a..6fa909d3411 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -3,7 +3,7 @@ #include #include -#define CHATTERINO_VERSION "7.3.2" +#define CHATTERINO_VERSION "7.3.3" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index b306a2a135c..9147bbea6bd 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -405,6 +405,16 @@ void Image::actuallyLoad() QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); QImageReader reader(&buffer); + + if (reader.size().width() * reader.size().height() * + reader.imageCount() * 4 > + Image::maxBytesRam) + { + qCDebug(chatterinoImage) << "image too large in RAM"; + + return Failure; + } + auto parsed = detail::readFrames(reader, shared->url()); postToThread(makeConvertCallback(parsed, [weak](auto frames) { diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index 193ba578709..a4ad674fe62 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -50,6 +50,9 @@ using ImagePtr = std::shared_ptr; class Image : public std::enable_shared_from_this, boost::noncopyable { public: + // Maximum amount of RAM used by the image in bytes. + static constexpr int maxBytesRam = 20 * 1024 * 1024; + ~Image(); static ImagePtr fromUrl(const Url &url, qreal scale = 1); diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 82c76f05235..9623d793759 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -51,6 +51,7 @@ class MessageBuilder MessageBuilder(const BanAction &action, uint32_t count = 1); MessageBuilder(const UnbanAction &action); MessageBuilder(const AutomodUserAction &action); + virtual ~MessageBuilder() = default; Message *operator->(); Message &message(); diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp index e3faa664859..e3f1a180059 100644 --- a/src/messages/SharedMessageBuilder.cpp +++ b/src/messages/SharedMessageBuilder.cpp @@ -107,6 +107,11 @@ void SharedMessageBuilder::parse() { this->parseUsernameColor(); + if (this->action_) + { + this->textColor_ = this->usernameColor_; + } + this->parseUsername(); this->message().flags.set(MessageFlag::Collapsed); @@ -408,8 +413,7 @@ void SharedMessageBuilder::addTextOrEmoji(const QString &string_) // Actually just text auto linkString = this->matchLink(string); auto link = Link(); - auto textColor = this->action_ ? MessageColor(this->usernameColor_) - : MessageColor(MessageColor::Text); + auto &&textColor = this->textColor_; if (linkString.isEmpty()) { diff --git a/src/messages/SharedMessageBuilder.hpp b/src/messages/SharedMessageBuilder.hpp index 49724b39633..2bee11fcdcd 100644 --- a/src/messages/SharedMessageBuilder.hpp +++ b/src/messages/SharedMessageBuilder.hpp @@ -2,6 +2,7 @@ #include "common/Aliases.hpp" #include "common/Outcome.hpp" +#include "messages/MessageColor.hpp" #include #include @@ -59,7 +60,8 @@ class SharedMessageBuilder : public MessageBuilder const bool action_{}; - QColor usernameColor_; + QColor usernameColor_ = {153, 153, 153}; + MessageColor textColor_ = MessageColor::Text; bool highlightAlert_ = false; bool highlightSound_ = false; diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp index 916cf544e19..ff52fa8479f 100644 --- a/src/providers/irc/IrcConnection2.cpp +++ b/src/providers/irc/IrcConnection2.cpp @@ -5,9 +5,6 @@ namespace chatterino { -// The minimum interval between attempting to establish a new connection -const int RECONNECT_MIN_INTERVAL = 15000; - namespace { const auto payload = QString("chatterino/" CHATTERINO_VERSION); @@ -48,18 +45,11 @@ IrcConnection::IrcConnection(QObject *parent) return; } - auto delta = - std::chrono::duration_cast( - std::chrono::steady_clock::now() - this->lastConnected_) - .count(); - delta = delta < RECONNECT_MIN_INTERVAL - ? (RECONNECT_MIN_INTERVAL - delta) - : 10; - qCDebug(chatterinoIrc) << "Reconnecting in" << delta << "ms"; - this->reconnectTimer_.start(delta); + auto delay = this->reconnectBackoff_.next(); + qCDebug(chatterinoIrc) << "Reconnecting in" << delay.count() << "ms"; + this->reconnectTimer_.start(delay); }); - this->reconnectTimer_.setInterval(RECONNECT_MIN_INTERVAL); this->reconnectTimer_.setSingleShot(true); QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] { if (this->isConnected()) @@ -120,13 +110,12 @@ IrcConnection::IrcConnection(QObject *parent) [this](Communi::IrcMessage *message) { // This connection is probably still alive this->recentlyReceivedMessage_ = true; + this->reconnectBackoff_.reset(); }); } void IrcConnection::open() { - // Accurately track the time a connection was opened - this->lastConnected_ = std::chrono::steady_clock::now(); this->expectConnectionLoss_ = false; this->waitingForPong_ = false; this->recentlyReceivedMessage_ = false; diff --git a/src/providers/irc/IrcConnection2.hpp b/src/providers/irc/IrcConnection2.hpp index 930b5dc4a0b..d3426c02922 100644 --- a/src/providers/irc/IrcConnection2.hpp +++ b/src/providers/irc/IrcConnection2.hpp @@ -1,10 +1,11 @@ #pragma once +#include "util/ExponentialBackoff.hpp" + #include #include #include -#include namespace chatterino { @@ -28,7 +29,9 @@ class IrcConnection : public Communi::IrcConnection QTimer pingTimer_; QTimer reconnectTimer_; std::atomic recentlyReceivedMessage_{true}; - std::chrono::steady_clock::time_point lastConnected_; + + // Reconnect with a base delay of 1 second and max out at 1 second * (2^4) (i.e. 16 seconds) + ExponentialBackoff<4> reconnectBackoff_{std::chrono::milliseconds{1000}}; std::atomic expectConnectionLoss_{false}; diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp index c7529a5e5e9..aa93e7f63a2 100644 --- a/src/providers/irc/IrcMessageBuilder.cpp +++ b/src/providers/irc/IrcMessageBuilder.cpp @@ -21,7 +21,6 @@ IrcMessageBuilder::IrcMessageBuilder( const MessageParseArgs &_args) : SharedMessageBuilder(_channel, _ircMessage, _args) { - this->usernameColor_ = getApp()->themes->messages.textColors.system; } IrcMessageBuilder::IrcMessageBuilder(Channel *_channel, @@ -31,7 +30,6 @@ IrcMessageBuilder::IrcMessageBuilder(Channel *_channel, : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction) { assert(false); - this->usernameColor_ = getApp()->themes->messages.textColors.system; } MessagePtr IrcMessageBuilder::build() diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 6f70386f3cc..580ee89ba52 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -358,7 +358,6 @@ void TwitchAccount::loadUserstateEmotes() name[0] = name[0].toUpper(); newUserEmoteSet->text = name; - newUserEmoteSet->type = QString(); newUserEmoteSet->channelName = ivrEmoteSet.login; for (const auto &emote : ivrEmoteSet.emotes) @@ -508,38 +507,48 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) return; } - NetworkRequest(Env::get().twitchEmoteSetResolverUrl.arg(emoteSet->key)) - .cache() - .onSuccess([emoteSet](NetworkResult result) -> Outcome { - auto root = result.parseJson(); - if (root.isEmpty()) + getHelix()->getEmoteSetData( + emoteSet->key, + [emoteSet](HelixEmoteSetData emoteSetData) { + if (emoteSetData.ownerId.isEmpty() || + emoteSetData.setId != emoteSet->key) { - return Failure; + qCWarning(chatterinoTwitch) + << QString("Failed to fetch emoteSetData for %1, assuming " + "Twitch is the owner") + .arg(emoteSet->key); + + // most (if not all) emotes that fail to load are time limited event emotes owned by Twitch + emoteSet->channelName = "twitch"; + emoteSet->text = "Twitch"; + + return; } - TwitchEmoteSetResolverResponse response(root); - - auto name = response.channelName; - name.detach(); - name[0] = name[0].toUpper(); - - emoteSet->text = name; - emoteSet->type = response.type; - emoteSet->channelName = response.channelName; - - qCDebug(chatterinoTwitch) - << QString("Loaded twitch emote set data for %1") - .arg(emoteSet->key); - - return Success; - }) - .onError([emoteSet](NetworkResult result) { - qCWarning(chatterinoTwitch) - << QString("Error code %1 while loading emote set data for %2") - .arg(result.status()) - .arg(emoteSet->key); - }) - .execute(); + // emote set 0 = global emotes + if (emoteSetData.ownerId == "0") + { + // emoteSet->channelName = QString(); + emoteSet->text = "Twitch Global"; + return; + } + + getHelix()->getUserById( + emoteSetData.ownerId, + [emoteSet](HelixUser user) { + emoteSet->channelName = user.login; + emoteSet->text = user.displayName; + }, + [emoteSetData] { + qCWarning(chatterinoTwitch) + << "Failed to query user by id:" << emoteSetData.ownerId + << emoteSetData.setId; + }); + }, + [emoteSet] { + // fetching emoteset data failed + return; + }); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index 761a583edb1..fc1918b1e9b 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -62,7 +62,6 @@ class TwitchAccount : public Account QString key; QString channelName; QString text; - QString type; std::vector emotes; }; diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 0d9ff27fd5a..ee68bfc3c4c 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -118,7 +118,6 @@ TwitchMessageBuilder::TwitchMessageBuilder( : SharedMessageBuilder(_channel, _ircMessage, _args) , twitchChannel(dynamic_cast(_channel)) { - this->usernameColor_ = getApp()->themes->messages.textColors.system; } TwitchMessageBuilder::TwitchMessageBuilder( @@ -127,7 +126,6 @@ TwitchMessageBuilder::TwitchMessageBuilder( : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction) , twitchChannel(dynamic_cast(_channel)) { - this->usernameColor_ = getApp()->themes->messages.textColors.system; } bool TwitchMessageBuilder::isIgnored() const @@ -470,8 +468,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) // Actually just text auto linkString = this->matchLink(string); - auto textColor = this->action_ ? MessageColor(this->usernameColor_) - : MessageColor(MessageColor::Text); + auto textColor = this->textColor_; if (!linkString.isEmpty()) { @@ -727,18 +724,15 @@ void TwitchMessageBuilder::appendUsername() // Separator this->emplace("->", MessageElementFlag::Username, - app->themes->messages.textColors.system, - FontStyle::ChatMedium); + MessageColor::System, FontStyle::ChatMedium); QColor selfColor = currentUser->color(); - if (!selfColor.isValid()) - { - selfColor = app->themes->messages.textColors.system; - } + MessageColor selfMsgColor = + selfColor.isValid() ? selfColor : MessageColor::System; // Your own username this->emplace(currentUser->getUserName() + ":", - MessageElementFlag::Username, selfColor, + MessageElementFlag::Username, selfMsgColor, FontStyle::ChatMediumBold); } else diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index a105914a31e..c430b59cfe1 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -761,6 +761,38 @@ void Helix::getCheermotes( .execute(); } +void Helix::getEmoteSetData(QString emoteSetId, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + + urlQuery.addQueryItem("emote_set_id", emoteSetId); + + this->makeRequest("chat/emotes/set", urlQuery) + .onSuccess([successCallback, failureCallback, + emoteSetId](auto result) -> Outcome { + QJsonObject root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return Failure; + } + + HelixEmoteSetData emoteSetData(data.toArray()[0].toObject()); + + successCallback(emoteSetData); + return Success; + }) + .onError([failureCallback](NetworkResult result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) { assert(!url.startsWith("/")); diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 6e02739180e..a4f745a7585 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -264,6 +264,17 @@ struct HelixCheermoteSet { } }; +struct HelixEmoteSetData { + QString setId; + QString ownerId; + + explicit HelixEmoteSetData(QJsonObject jsonObject) + : setId(jsonObject.value("emote_set_id").toString()) + , ownerId(jsonObject.value("owner_id").toString()) + { + } +}; + enum class HelixClipError { Unknown, ClipsDisabled, @@ -398,6 +409,11 @@ class Helix final : boost::noncopyable ResultCallback> successCallback, HelixFailureCallback failureCallback); + // https://dev.twitch.tv/docs/api/reference#get-emote-sets + void getEmoteSetData(QString emoteSetId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + void update(QString clientId, QString oauthToken); static void initialize(); diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 9b24e8f7e04..2a4a2dede90 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -157,6 +157,14 @@ URL: https://dev.twitch.tv/docs/api/reference/#get-cheermotes Used in: - `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000` +### Get Emote Sets + +URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets + +- We implement this in `providers/twitch/api/Helix.cpp getEmoteSetData` + Used in: + - `providers/twitch/TwitchAccount.cpp` to set emoteset owner data upon loading subscriber emotes from Kraken + ## TMI The TMI api is undocumented. diff --git a/src/util/ExponentialBackoff.hpp b/src/util/ExponentialBackoff.hpp new file mode 100644 index 00000000000..44eca67e1bd --- /dev/null +++ b/src/util/ExponentialBackoff.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +namespace chatterino { + +// Yes, you can't specify the base 😎 deal with it +template +class ExponentialBackoff +{ +public: + /** + * Creates an object helping you make exponentially (with base 2) backed off times. + * + * @param start The start time in milliseconds + * @param maxSteps The max number of progressions we will take before stopping + * + * For example, ExponentialBackoff(10ms, 3) would have the next() function return 10ms, 20ms, 40ms, 40ms, ..., 40ms + **/ + ExponentialBackoff(const std::chrono::milliseconds &start) + : start_(start) + , step_{1} + { + static_assert(maxSteps > 1, "maxSteps must be higher than 1"); + } + + /** + * Return the current number in the progression and increment the step until the next one (assuming we're not at the cap) + * + * @returns current step in milliseconds + **/ + [[nodiscard]] std::chrono::milliseconds next() + { + auto next = this->start_ * (1 << (this->step_ - 1)); + + this->step_ += 1; + + if (this->step_ >= maxSteps) + { + this->step_ = maxSteps; + } + + return next; + } + + /** + * Reset the progression back to its initial state + **/ + void reset() + { + this->step_ = 1; + } + +private: + const std::chrono::milliseconds start_; + unsigned step_; +}; + +} // namespace chatterino diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 0d551e14743..1d1bbe7854b 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -352,10 +352,10 @@ void Notebook::setShowTabs(bool value) { QMessageBox msgBox; msgBox.window()->setWindowTitle("Chatterino - hidden tabs"); - msgBox.setText("You've just hidden your tabs"); + msgBox.setText("You've just hidden your tabs."); msgBox.setInformativeText( - "You can toggle tabs by using the keyboard shortcut (Ctrl + U by " - "default) or right-clicking on the tab area and selecting \"Toggle " + "You can toggle tabs by using the keyboard shortcut (Ctrl+U by " + "default) or right-clicking the tab area and selecting \"Toggle " "visibility of tabs\"."); msgBox.addButton(QMessageBox::Ok); auto *dsaButton = diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 2f2291aade5..fec62ba4e5f 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -71,8 +71,7 @@ namespace { { // TITLE auto channelName = set->channelName; - auto text = - set->key == "0" || set->text.isEmpty() ? "Twitch" : set->text; + auto text = set->text.isEmpty() ? "Twitch" : set->text; // EMOTES MessageBuilder builder; diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index b27e2e6980b..3c82d43f4f5 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -6,8 +6,8 @@ #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightBlacklistUser.hpp" #include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" #include "providers/IvrApi.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Kraken.hpp" diff --git a/src/widgets/settingspages/FiltersPage.cpp b/src/widgets/settingspages/FiltersPage.cpp index 3fd8ef9543a..b5282db8286 100644 --- a/src/widgets/settingspages/FiltersPage.cpp +++ b/src/widgets/settingspages/FiltersPage.cpp @@ -12,7 +12,7 @@ #include -#define FILTERS_DOCUMENTATION "https://wiki.chatterino.com/Filters/" +#define FILTERS_DOCUMENTATION "https://wiki.chatterino.com/Filters" namespace chatterino { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8a13c128d44..52e8560f7b1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/ChatterSet.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HighlightPhrase.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ExponentialBackoff.cpp ) add_executable(${PROJECT_NAME} ${test_SOURCES}) diff --git a/tests/src/ExponentialBackoff.cpp b/tests/src/ExponentialBackoff.cpp new file mode 100644 index 00000000000..2a4259744a1 --- /dev/null +++ b/tests/src/ExponentialBackoff.cpp @@ -0,0 +1,58 @@ +#include "util/ExponentialBackoff.hpp" + +#include + +using namespace chatterino; + +TEST(ExponentialBackoff, MaxSteps) +{ + using namespace std::literals::chrono_literals; + + ExponentialBackoff<3> foo{10ms}; + + // First usage should be the start value + EXPECT_EQ(foo.next(), 10ms); + EXPECT_EQ(foo.next(), 20ms); + EXPECT_EQ(foo.next(), 40ms); + // We reached the max steps, so we should continue returning the max value without increasing + EXPECT_EQ(foo.next(), 40ms); + EXPECT_EQ(foo.next(), 40ms); + EXPECT_EQ(foo.next(), 40ms); +} + +TEST(ExponentialBackoff, Reset) +{ + using namespace std::literals::chrono_literals; + + ExponentialBackoff<3> foo{10ms}; + + // First usage should be the start value + EXPECT_EQ(foo.next(), 10ms); + EXPECT_EQ(foo.next(), 20ms); + EXPECT_EQ(foo.next(), 40ms); + // We reached the max steps, so we should continue returning the max value without increasing + EXPECT_EQ(foo.next(), 40ms); + EXPECT_EQ(foo.next(), 40ms); + EXPECT_EQ(foo.next(), 40ms); + + foo.reset(); + + // After a reset, we should start at the beginning value again + EXPECT_EQ(foo.next(), 10ms); + EXPECT_EQ(foo.next(), 20ms); + EXPECT_EQ(foo.next(), 40ms); + // We reached the max steps, so we should continue returning the max value without increasing + EXPECT_EQ(foo.next(), 40ms); + EXPECT_EQ(foo.next(), 40ms); + EXPECT_EQ(foo.next(), 40ms); +} + +TEST(ExponentialBackoff, BadMaxSteps) +{ + using namespace std::literals::chrono_literals; + + // this will not compile + // ExponentialBackoff<1> foo{10ms}; + // ExponentialBackoff<0> foo{10ms}; + // ExponentialBackoff<-1> foo{10ms}; +}