Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/curl override query responses mode for test #429

Merged
merged 2 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions data/secret/secret_test.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{
"bithumb": {
"cha": {
"accountOwner": {
"enName": "ChaSonghyon",
"koName": "차성현"
},
"key": "test0000/0000000000000000+00000000000000000000000+000000",
"private": "abc/defghijkl+mnopqrstuvwxyzABCDEFGHIJKLMNOPQ+RSTUVWXYZ012345678+9/abcdefghijklmnopqrs=="
}
},
"kraken": {
"jack": {
"key": "test1000/0000000000000000+00000000000000000000000+000000",
Expand Down
14 changes: 5 additions & 9 deletions src/api-objects/src/apikeysprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ namespace cct::api {
namespace {

std::string_view GetSecretFileName(settings::RunMode runMode) {
switch (runMode) {
case settings::RunMode::kTestKeys:
[[fallthrough]];
case settings::RunMode::kTestKeysWithProxy:
log::info("Test mode activated, shifting to secret_test.json file.");
return "secret_test.json";
default:
return "secret.json";
if (settings::AreTestKeysRequested(runMode)) {
log::info("Test mode activated, shifting to secret_test.json file.");
return "secret_test.json";
}
return "secret.json";
}

} // namespace
Expand Down Expand Up @@ -75,7 +71,7 @@ APIKeysProvider::APIKeysMap APIKeysProvider::ParseAPIKeys(std::string_view dataD
} else {
std::string_view secretFileName = GetSecretFileName(runMode);
File secretsFile(dataDir, File::Type::kSecret, secretFileName,
AreTestKeysRequested(runMode) ? File::IfError::kThrow : File::IfError::kNoThrow);
settings::AreTestKeysRequested(runMode) ? File::IfError::kThrow : File::IfError::kNoThrow);
json jsonData = secretsFile.readAllJson();
for (auto& [publicExchangeName, keyObj] : jsonData.items()) {
const auto& exchangesWithoutSecrets = exchangeSecretsInfo.exchangesWithoutSecrets();
Expand Down
2 changes: 1 addition & 1 deletion src/api/common/src/cryptowatchapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace cct::api {
namespace {
string Query(CurlHandle& curlHandle, std::string_view endpoint, CurlPostData&& postData = CurlPostData()) {
std::string_view Query(CurlHandle& curlHandle, std::string_view endpoint, CurlPostData&& postData = CurlPostData()) {
return curlHandle.query(endpoint,
CurlOptions(HttpRequestType::kGet, std::move(postData), "Cryptowatch C++ API Client"));
}
Expand Down
5 changes: 3 additions & 2 deletions src/api/common/src/fiatconverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ std::optional<double> FiatConverter::queryCurrencyRate(Market mk) {
method.append(opts.getPostData().str());
opts.getPostData().clear();

string dataStr = _curlHandle.query(method, opts);
json data = json::parse(dataStr, nullptr, false /* allow exceptions */);
std::string_view dataStr = _curlHandle.query(method, opts);
static constexpr bool kAllowExceptions = false;
json data = json::parse(dataStr, nullptr, kAllowExceptions);
//{"query":{"count":1},"results":{"EUR_KRW":{"id":"EUR_KRW","val":1329.475323,"to":"KRW","fr":"EUR"}}}
if (data == json::value_t::discarded || !data.contains("results") || !data["results"].contains(qStr)) {
log::error("No JSON data received from fiat currency converter service");
Expand Down
5 changes: 3 additions & 2 deletions src/api/common/test/fiatconverter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ CurlHandle::CurlHandle([[maybe_unused]] const BestURLPicker &bestURLPicker,
: _handle(nullptr), _bestUrlPicker(kSomeFakeURL) {}

// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
string CurlHandle::query(std::string_view endpoint, [[maybe_unused]] const CurlOptions &opts) {
std::string_view CurlHandle::query(std::string_view endpoint, [[maybe_unused]] const CurlOptions &opts) {
json jsonData;
if (endpoint.find("currencies") != std::string_view::npos) {
// Currencies
Expand Down Expand Up @@ -69,7 +69,8 @@ string CurlHandle::query(std::string_view endpoint, [[maybe_unused]] const CurlO
jsonData["results"][marketStr]["val"] = rate;
}
}
return jsonData.dump();
_queryData = jsonData.dump();
return _queryData;
}

CurlHandle::~CurlHandle() {} // NOLINT
Expand Down
5 changes: 5 additions & 0 deletions src/api/exchanges/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ add_exchange_test(
test/bithumbapi_test.cpp
)

add_exchange_test(
bithumb_place_order_test
test/bithumb_place_order_test.cpp
)

add_exchange_test(
huobiapi_test
test/huobiapi_test.cpp
Expand Down
2 changes: 2 additions & 0 deletions src/api/exchanges/include/bithumbprivateapi.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class BithumbPrivate : public ExchangePrivate {
InitiatedWithdrawInfo launchWithdraw(MonetaryAmount grossAmount, Wallet&& destinationWallet) override;

private:
friend class BithumbPrivateAPIPlaceOrderTest;

struct DepositWalletFunc {
#ifndef CCT_AGGR_INIT_CXX20
DepositWalletFunc(CurlHandle& curlHandle, const APIKey& apiKey, BithumbPublic& exchangePublic)
Expand Down
8 changes: 6 additions & 2 deletions src/api/exchanges/src/binancepublicapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,14 @@ BinancePublic::ExchangeInfoFunc::ExchangeInfoDataByMarket BinancePublic::Exchang
}

json BinancePublic::GlobalInfosFunc::operator()() {
string dataStr = _curlHandle.query("", CurlOptions(HttpRequestType::kGet));
string dataStr = _curlHandle.queryRelease("", CurlOptions(HttpRequestType::kGet));
// This json is HUGE and contains numerous amounts of information
static constexpr std::string_view appBegJson = "application/json\">";
string::const_iterator first = dataStr.begin() + dataStr.find(appBegJson) + appBegJson.size();
std::size_t beg = dataStr.find(appBegJson);
if (beg == string::npos) {
throw exception("Unexpected answer from {}", _curlHandle.getNextBaseUrl());
}
string::const_iterator first = dataStr.begin() + beg + appBegJson.size();
std::string_view sv(first, dataStr.end());
std::size_t reduxPos = sv.find("redux\":");
std::size_t ssrStorePos = sv.find("ssrStore\":", reduxPos);
Expand Down
51 changes: 27 additions & 24 deletions src/api/exchanges/src/bithumbprivateapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,12 @@ constexpr std::string_view kWalletAddressEndpointStr = "/info/wallet_address";

std::pair<string, Nonce> GetStrData(std::string_view endpoint, std::string_view postDataStr) {
Nonce nonce = Nonce_TimeSinceEpochInMs();
string strData(endpoint);
strData.reserve(strData.size() + 2U + postDataStr.size() + nonce.size());

static constexpr char kParChar = 1;
strData.push_back(kParChar);
strData.append(postDataStr);
strData.push_back(kParChar);
string strData(endpoint.size() + 2U + postDataStr.size() + nonce.size(), kParChar);

strData.append(nonce.begin(), nonce.end());
auto it = std::ranges::copy(endpoint, strData.begin()).out;
it = std::ranges::copy(postDataStr, it + 1).out;
it = std::ranges::copy(nonce, it + 1).out;
return std::make_pair(std::move(strData), std::move(nonce));
}

Expand Down Expand Up @@ -276,19 +273,21 @@ BithumbPrivate::BithumbPrivate(const CoincenterInfo& config, BithumbPublic& bith
_depositWalletsCache(
CachedResultOptions(exchangeInfo().getAPICallUpdateFrequency(kDepositWallet), _cachedResultVault),
_curlHandle, _apiKey, bithumbPublic) {
json data = GetBithumbCurrencyInfoMapCache(_coincenterInfo.dataDir()).readAllJson();
for (const auto& [currencyCodeStr, currencyOrderInfoJson] : data.items()) {
CurrencyOrderInfo currencyOrderInfo;

LoadCurrencyInfoField(currencyOrderInfoJson, kNbDecimalsStr, currencyOrderInfo.nbDecimals,
currencyOrderInfo.lastNbDecimalsUpdatedTime);
LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderSizeJsonKeyStr, currencyOrderInfo.minOrderSize,
currencyOrderInfo.lastMinOrderSizeUpdatedTime);
LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderPriceJsonKeyStr, currencyOrderInfo.minOrderPrice,
currencyOrderInfo.lastMinOrderPriceUpdatedTime);
LoadCurrencyInfoField(currencyOrderInfoJson, kMaxOrderPriceJsonKeyStr, currencyOrderInfo.maxOrderPrice,
currencyOrderInfo.lastMaxOrderPriceUpdatedTime);
_currencyOrderInfoMap.insert_or_assign(CurrencyCode(currencyCodeStr), std::move(currencyOrderInfo));
if (config.getRunMode() != settings::RunMode::kQueryResponseOverriden) {
json data = GetBithumbCurrencyInfoMapCache(_coincenterInfo.dataDir()).readAllJson();
for (const auto& [currencyCodeStr, currencyOrderInfoJson] : data.items()) {
CurrencyOrderInfo currencyOrderInfo;

LoadCurrencyInfoField(currencyOrderInfoJson, kNbDecimalsStr, currencyOrderInfo.nbDecimals,
currencyOrderInfo.lastNbDecimalsUpdatedTime);
LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderSizeJsonKeyStr, currencyOrderInfo.minOrderSize,
currencyOrderInfo.lastMinOrderSizeUpdatedTime);
LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderPriceJsonKeyStr, currencyOrderInfo.minOrderPrice,
currencyOrderInfo.lastMinOrderPriceUpdatedTime);
LoadCurrencyInfoField(currencyOrderInfoJson, kMaxOrderPriceJsonKeyStr, currencyOrderInfo.maxOrderPrice,
currencyOrderInfo.lastMaxOrderPriceUpdatedTime);
_currencyOrderInfoMap.insert_or_assign(CurrencyCode(currencyCodeStr), std::move(currencyOrderInfo));
}
}
}

Expand Down Expand Up @@ -757,17 +756,21 @@ PlaceOrderInfo BithumbPrivate::placeOrder(MonetaryAmount /*from*/, MonetaryAmoun
if (LoadCurrencyInfoField(result, kNbDecimalsStr, currencyOrderInfo.nbDecimals,
currencyOrderInfo.lastNbDecimalsUpdatedTime)) {
volume.truncate(currencyOrderInfo.nbDecimals);
if (volume == 0) {
log::warn("No trade of {} into {} because volume is 0 after truncating to {} decimals", volume,
toCurrencyCode, static_cast<int>(currencyOrderInfo.nbDecimals));
break;
}
placePostData.set("units", volume.amountStr());
} else if (LoadCurrencyInfoField(result, kMinOrderSizeJsonKeyStr, currencyOrderInfo.minOrderSize,
currencyOrderInfo.lastMinOrderSizeUpdatedTime)) {
if (isSimulationWithRealOrder && currencyOrderInfo.minOrderSize.currencyCode() == price.currencyCode()) {
volume = MonetaryAmount(currencyOrderInfo.minOrderSize / price, volume.currencyCode());
placePostData.set("units", volume.amountStr());
} else {
if (!isSimulationWithRealOrder || currencyOrderInfo.minOrderSize.currencyCode() != price.currencyCode()) {
log::warn("No trade of {} into {} because min order size is {} for this market", volume, toCurrencyCode,
currencyOrderInfo.minOrderSize);
break;
}
volume = MonetaryAmount(currencyOrderInfo.minOrderSize / price, volume.currencyCode());
placePostData.set("units", volume.amountStr());
} else if (LoadCurrencyInfoField(result, kMinOrderPriceJsonKeyStr, currencyOrderInfo.minOrderPrice,
currencyOrderInfo.lastMinOrderPriceUpdatedTime)) {
if (isSimulationWithRealOrder) {
Expand Down
2 changes: 1 addition & 1 deletion src/api/exchanges/src/bithumbpublicapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ WithdrawalFeeMap BithumbPublic::WithdrawalFeesFunc::operator()() {
WithdrawalFeeMap ret;
// This is not a published API and only a "standard" html page. We will capture the text information in it.
// Warning, it's not in json format so we will need manual parsing.
string dataStr = _curlHandle.query("/customer_support/info_fee", CurlOptions(HttpRequestType::kGet));
std::string_view dataStr = _curlHandle.query("/customer_support/info_fee", CurlOptions(HttpRequestType::kGet));
// Now, we have the big string containing the html data. The following should work as long as format is unchanged.
// here is a line containing our coin with its additional withdrawal fees:
//
Expand Down
4 changes: 2 additions & 2 deletions src/api/exchanges/src/krakenpublicapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ KrakenPublic::WithdrawalFeesFunc::WithdrawalFeesFunc(const CoincenterInfo& coinc
coincenterInfo.getRunMode()) {}

KrakenPublic::WithdrawalFeesFunc::WithdrawalInfoMaps KrakenPublic::WithdrawalFeesFunc::updateFromSource1() {
string withdrawalFeesCsv = _curlHandle1.query("", CurlOptions(HttpRequestType::kGet));
std::string_view withdrawalFeesCsv = _curlHandle1.query("", CurlOptions(HttpRequestType::kGet));

static constexpr std::string_view kBeginWithdrawalFeeHtmlTag = "<td class=withdrawalFee>";
static constexpr std::string_view kBeginMinWithdrawalHtmlTag = "<td class=minWithdrawal>";
Expand Down Expand Up @@ -235,7 +235,7 @@ KrakenPublic::WithdrawalFeesFunc::WithdrawalInfoMaps KrakenPublic::WithdrawalFee
}

KrakenPublic::WithdrawalFeesFunc::WithdrawalInfoMaps KrakenPublic::WithdrawalFeesFunc::updateFromSource2() {
string withdrawalFeesCsv = _curlHandle2.query("", CurlOptions(HttpRequestType::kGet));
std::string_view withdrawalFeesCsv = _curlHandle2.query("", CurlOptions(HttpRequestType::kGet));

static constexpr std::string_view kBeginTableTitle = "Kraken Deposit & Withdrawal fees</h2>";

Expand Down
69 changes: 69 additions & 0 deletions src/api/exchanges/test/bithumb_place_order_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#include <gtest/gtest.h>

#include "apikeysprovider.hpp"
#include "bithumbprivateapi.hpp"
#include "bithumbpublicapi.hpp"
#include "coincenterinfo.hpp"
#include "fiatconverter.hpp"

namespace cct::api {
class BithumbPrivateAPIPlaceOrderTest : public ::testing::Test {
protected:
PlaceOrderInfo placeOrder(MonetaryAmount volume, MonetaryAmount price, TradeSide tradeSide) {
Market market{volume.currencyCode(), price.currencyCode()};
TradeContext tradeContext{market, tradeSide};
TradeOptions tradeOptions;
TradeInfo tradeInfo{tradeContext, tradeOptions};

return exchangePrivate.placeOrder(from, volume, price, tradeInfo);
}

void setOverridenQueryResponses(const std::map<string, string>& queryResponses) {
exchangePrivate._curlHandle.setOverridenQueryResponses(queryResponses);
}

settings::RunMode runMode = settings::RunMode::kQueryResponseOverriden;
LoadConfiguration loadConfig{kDefaultDataDir, LoadConfiguration::ExchangeConfigFileType::kTest};
CoincenterInfo coincenterInfo{runMode, loadConfig};
FiatConverter fiatConverter{coincenterInfo, Duration::max()}; // max to avoid real Fiat converter queries
CryptowatchAPI cryptowatchAPI{coincenterInfo, runMode};
BithumbPublic exchangePublic{coincenterInfo, fiatConverter, cryptowatchAPI};
APIKeysProvider apiKeysProvider{coincenterInfo.dataDir(), coincenterInfo.getRunMode()};
ExchangeName exchangeName{exchangePublic.name(), apiKeysProvider.getKeyNames(exchangePublic.name()).front()};
const APIKey& testKey = apiKeysProvider.get(exchangeName);
BithumbPrivate exchangePrivate{coincenterInfo, exchangePublic, testKey};

MonetaryAmount from;
};

TEST_F(BithumbPrivateAPIPlaceOrderTest, PlaceOrderShortenDecimals) {
setOverridenQueryResponses(
{/// Place order, with high number of decimals
{"/trade/"
"place?endpoint=%2Ftrade%2Fplace&order_currency=ETH&payment_currency=EUR&type=ask&price=1500&units=2.000001",
"{\"status\": \"5600\", \"message\":\"수량은 소수점 4자\"}"},
/// Replace order with decimals correctly truncated
{"/trade/"
"place?endpoint=%2Ftrade%2Fplace&order_currency=ETH&payment_currency=EUR&type=ask&price=1500&units=2",
"{\"status\": \"0000\", \"order_id\": \"ID0001\"}"},
/// Query once order info, order not matched
{"/info/orders?endpoint=%2Finfo%2Forders&order_currency=ETH&payment_currency=EUR&type=ask&order_id=ID0001",
"{\"status\": \"0000\", \"data\": [{\"order_id\": \"ID0001\"}]}"}});

PlaceOrderInfo placeOrderInfo =
placeOrder(MonetaryAmount("2.000001ETH"), MonetaryAmount("1500EUR"), TradeSide::kSell);
EXPECT_EQ(placeOrderInfo.orderId, "ID0001");
}

TEST_F(BithumbPrivateAPIPlaceOrderTest, NoPlaceOrderTooSmallAmount) {
setOverridenQueryResponses(
{/// Place order, with high number of decimals
{"/trade/"
"place?endpoint=%2Ftrade%2Fplace&order_currency=ETH&payment_currency=EUR&type=ask&price=1500&units=0.000001",
"{\"status\": \"5600\", \"message\":\"수량은 소수점 4자\"}"}});

PlaceOrderInfo placeOrderInfo =
placeOrder(MonetaryAmount("0.000001ETH"), MonetaryAmount("1500EUR"), TradeSide::kSell);
EXPECT_EQ(placeOrderInfo.orderId, "UndefinedId");
}
} // namespace cct::api
2 changes: 0 additions & 2 deletions src/engine/test/exchangedata_test.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ class ExchangesBaseTest : public ::testing::Test {
EXPECT_CALL(exchangePrivate8, queryAccountBalance(testing::_)).WillRepeatedly(testing::Return(emptyBalance));
}

void TearDown() override {}

LoadConfiguration loadConfiguration{kDefaultDataDir, LoadConfiguration::ExchangeConfigFileType::kTest};
settings::RunMode runMode = settings::RunMode::kTestKeys;
CoincenterInfo coincenterInfo{runMode, loadConfiguration};
Expand Down
2 changes: 2 additions & 0 deletions src/http-request/include/besturlpicker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class BestURLPicker {

int8_t nbBaseURL() const { return static_cast<int8_t>(_responseTimeStatsPerBaseUrl.size()); }

int nbRequestsDone() const;

private:
explicit BestURLPicker(std::span<const std::string_view> baseUrls)
: _pBaseUrls(baseUrls.data()), _responseTimeStatsPerBaseUrl(baseUrls.size()) {}
Expand Down
18 changes: 16 additions & 2 deletions src/http-request/include/curlhandle.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <map>
#include <string_view>
#include <type_traits>
#include <utility>
Expand Down Expand Up @@ -34,23 +35,35 @@ class CurlHandle {
Duration minDurationBetweenQueries = Duration::zero(),
settings::RunMode runMode = settings::RunMode::kProd);

// Move operations are deleted but could be implemented if needed. It's just to avoid useless code.
CurlHandle(const CurlHandle &) = delete;
CurlHandle &operator=(const CurlHandle &) = delete;

// Move operations are deleted but could be implemented if needed. It's just to avoid useless code.
CurlHandle(CurlHandle &&) = delete;
CurlHandle &operator=(CurlHandle &&) = delete;

~CurlHandle();

/// Launch a query on the given endpoint, it should start with a '/' and not contain the base URLs given at
/// creation of this object.
string query(std::string_view endpoint, const CurlOptions &opts);
/// Response is returned as a std::string_view to a memory hold in cache by this CurlHandle.
/// The pointed memory is valid until a next call to 'query'.
std::string_view query(std::string_view endpoint, const CurlOptions &opts);

/// Same as 'query' except that internal memory buffer is immediately freed after the query.
/// This can be useful for rare queries with very large responses for instance.
string queryRelease(std::string_view endpoint, const CurlOptions &opts);

std::string_view getNextBaseUrl() const { return _bestUrlPicker.getNextBaseURL(); }

Duration minDurationBetweenQueries() const { return _minDurationBetweenQueries; }

/// Instead of actually performing real calls, instructs this CurlHandle to
/// return hardcoded responses (in values of given map) based on query endpoints with appended options (in key of
/// given map) This should be used only for tests purposes, as the search for the matching query is of linear
/// complexity in a flat key value string.
void setOverridenQueryResponses(const std::map<string, string> &queryResponsesMap);

// CurlHandle is trivially relocatable
using trivially_relocatable = std::true_type;

Expand All @@ -64,6 +77,7 @@ class CurlHandle {
Duration _minDurationBetweenQueries;
TimePoint _lastQueryTime{};
BestURLPicker _bestUrlPicker;
string _queryData;
};

// Simple RAII class managing global init and clean up of Curl library.
Expand Down
10 changes: 7 additions & 3 deletions src/http-request/src/besturlpicker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ int8_t BestURLPicker::nextBaseURLPos() const {

// We favor the URL that has the least score for 90 % of the requests, and give a chance to the one with the least
// number of requests 10 % of the time, not counting the one with the best score.
int totalNbRequestsDone =
std::accumulate(_responseTimeStatsPerBaseUrl.begin(), _responseTimeStatsPerBaseUrl.end(), 0,
[](int sum, ResponseTimeStats stats) { return sum + stats.nbRequestsDone; });
int totalNbRequestsDone = nbRequestsDone();
if ((totalNbRequestsDone % 10) == 9) {
ResponseTimeStats minScoreResponseTimeStats = *nextBaseURLIt;

Expand Down Expand Up @@ -96,4 +94,10 @@ void BestURLPicker::storeResponseTimePerBaseURL(int8_t baseUrlPos, uint32_t resp
log::debug("Response time stats for '{}': Avg: {} ms, Dev: {} ms, Nb: {} (last: {} ms)", _pBaseUrls[baseUrlPos],
stats.avgResponseTime, stats.avgDeviation, stats.nbRequestsDone, responseTimeInMs);
}

int BestURLPicker::nbRequestsDone() const {
return std::accumulate(_responseTimeStatsPerBaseUrl.begin(), _responseTimeStatsPerBaseUrl.end(), 0,
[](int sum, ResponseTimeStats stats) { return sum + stats.nbRequestsDone; });
}

} // namespace cct
Loading
Loading