Initial commit

This commit is contained in:
dev
2025-02-27 21:53:53 +08:00
commit 815e55e4c0
1291 changed files with 185445 additions and 0 deletions

11
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,11 @@
add_subdirectory(lib)
add_subdirectory(main)
add_subdirectory(common)
add_subdirectory(analytics)
add_subdirectory(meta)
add_subdirectory(scripts)
add_subdirectory(client)
add_subdirectory(kv)
add_subdirectory(storage)
add_subdirectory(mgmtd)
add_subdirectory(migration)

193
tests/FakeMgmtdClient.h Normal file
View File

@@ -0,0 +1,193 @@
#include <folly/Random.h>
#include <folly/ScopeGuard.h>
#include <folly/Synchronized.h>
#include <folly/concurrency/AtomicSharedPtr.h>
#include <memory>
#include <set>
#include <vector>
#include "client/mgmtd/ICommonMgmtdClient.h"
#include "client/mgmtd/RoutingInfo.h"
#include "common/app/ClientId.h"
#include "common/utils/Address.h"
#include "common/utils/UtcTime.h"
#include "fbs/mgmtd/ChainRef.h"
#include "fbs/mgmtd/ChainTargetInfo.h"
#include "fbs/mgmtd/ClientSession.h"
#include "fbs/mgmtd/MgmtdTypes.h"
#include "fbs/mgmtd/RoutingInfo.h"
#include "fbs/mgmtd/Rpc.h"
#include "fbs/mgmtd/TargetInfo.h"
namespace hf3fs::tests {
class FakeMgmtdClient : public hf3fs::client::ICommonMgmtdClient {
public:
static std::shared_ptr<FakeMgmtdClient> create(
std::vector<std::tuple<flat::ChainTableId, size_t, size_t>> tables = {},
size_t numMetas = 1,
size_t numStorages = 1) {
auto mgmtd = std::make_shared<FakeMgmtdClient>();
for (auto [table, numChains, numReplica] : tables) {
mgmtd->addChainTable(table, numChains, numReplica);
}
mgmtd->addNodes(numMetas, numStorages);
for (size_t i = 0; i < 10; i++) {
mgmtd->addClient(Uuid::random());
}
return mgmtd;
}
FakeMgmtdClient()
: routingInfo_(std::make_shared<hf3fs::client::RoutingInfo>(std::make_shared<hf3fs::flat::RoutingInfo>(),
SteadyClock::now())) {}
std::shared_ptr<hf3fs::client::RoutingInfo> getRoutingInfo() override { return routingInfo_.load(); }
CoTryTask<void> refreshRoutingInfo(bool) override { co_return Void{}; }
bool addRoutingInfoListener(String name, RoutingInfoListener func) override {
listeners_[name] = func;
return true;
}
bool removeRoutingInfoListener(std::string_view name) override {
listeners_.erase(std::string(name));
return true;
}
CoTryTask<mgmtd::ListClientSessionsRsp> listClientSessions() override {
auto guard = sessions_.lock();
mgmtd::ListClientSessionsRsp rsp;
rsp.bootstrapping = false;
rsp.sessions = *guard;
for (auto &session : rsp.sessions) {
session.lastExtend = UtcClock::now();
}
co_return rsp;
}
std::set<Uuid> getActiveClients() {
std::set<Uuid> active;
auto guard = sessions_.lock();
for (const auto &client : *guard) {
active.insert(Uuid::fromHexString(client.clientId).value());
}
return active;
}
ClientId getOneActiveClient() {
auto guard = sessions_.lock();
return ClientId(Uuid::fromHexString(guard->at(folly::Random::rand32(guard->size())).clientId).value());
}
void clearActiveClient() { sessions_.lock()->clear(); }
void addClient(Uuid client) {
flat::ClientSession session;
session.clientId = client.toHexString();
sessions_.lock()->push_back(session);
}
CoTryTask<mgmtd::GetClientSessionRsp> getClientSession(const String &) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<std::optional<flat::ConfigInfo>> getConfig(flat::NodeType, flat::ConfigVersion) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<RHStringHashMap<flat::ConfigVersion>> getConfigVersions() override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<std::vector<flat::TagPair>> getUniversalTags(const String &universalId) override {
co_return makeError(StatusCode::kNotImplemented);
}
std::shared_ptr<hf3fs::client::RoutingInfo> cloneRoutingInfo() {
auto curr = routingInfo_.load();
auto next = std::make_shared<hf3fs::client::RoutingInfo>(std::make_shared<hf3fs::flat::RoutingInfo>(*curr->raw()),
SteadyClock::now());
next->raw()->routingInfoVersion++;
return next;
}
void setRoutingInfo(std::shared_ptr<hf3fs::client::RoutingInfo> next) {
routingInfo_.store(next);
for (auto &[name, func] : listeners_) {
func(routingInfo_.load());
}
}
void addChainTable(flat::ChainTableId tableId, size_t numChains, size_t numReplica = 1) {
auto next = cloneRoutingInfo();
SCOPE_EXIT { setRoutingInfo(next); };
auto &routing = next->raw();
if (!routing->chainTables.contains(tableId)) {
routing->chainTables[tableId] = {};
}
auto version = flat::ChainTableVersion(routing->chainTables[tableId].size() + 1);
auto &table = routing->chainTables[tableId][version];
table.chainTableVersion = version;
for (size_t index = 1; index <= numChains; index++) {
flat::ChainInfo chain;
chain.chainId = flat::ChainId(tableId * 100000 + index);
chain.chainVersion = flat::ChainVersion(1);
chain.targets = {};
for (size_t replica = 0; replica < numReplica; replica++) {
flat::ChainTargetInfo target;
target.targetId = flat::TargetId(folly::Random::rand32());
target.publicState = flat::PublicTargetState::SERVING;
chain.targets.push_back(target);
}
routing->chains[chain.chainId] = chain;
table.chains.push_back(chain.chainId);
}
}
void clearNodes() {
auto next = cloneRoutingInfo();
SCOPE_EXIT { setRoutingInfo(next); };
next->raw()->nodes.clear();
}
void addNodes(size_t numMetas, size_t numStorages) {
auto next = cloneRoutingInfo();
SCOPE_EXIT { setRoutingInfo(next); };
auto &routing = next->raw();
for (size_t i = 0; i < numMetas; i++) {
auto id = metaId_++;
flat::NodeInfo info;
info.app.nodeId = flat::NodeId(id);
info.app.hostname = fmt::format("host-{}", id);
info.type = flat::NodeType::META;
info.status = flat::NodeStatus::HEARTBEAT_CONNECTED;
info.app.serviceGroups.emplace_back(
std::set<String>{"MetaSerde"},
std::vector<net::Address>{net::Address::from(fmt::format("RDMA://127.0.0.1:{}", id)).value(),
net::Address::from(fmt::format("TCP://127.0.0.1:{}", id)).value()});
routing->nodes[flat::NodeId(id)] = info;
}
for (size_t i = 0; i < numStorages; i++) {
auto id = storageId_++;
flat::NodeInfo info;
info.app.nodeId = flat::NodeId(id);
info.app.hostname = fmt::format("host-{}", id);
info.type = flat::NodeType::STORAGE;
info.status = flat::NodeStatus::HEARTBEAT_CONNECTED;
info.app.serviceGroups.emplace_back(
std::set<String>{"Storage"},
std::vector<net::Address>{net::Address::from(fmt::format("RDMA://127.0.0.1:{}", id)).value(),
net::Address::from(fmt::format("TCP://127.0.0.1:{}", id)).value()});
routing->nodes[flat::NodeId(id)] = info;
}
}
private:
folly::Synchronized<std::vector<flat::ClientSession>, std::mutex> sessions_;
folly::atomic_shared_ptr<client::RoutingInfo> routingInfo_;
std::map<std::string, RoutingInfoListener> listeners_;
size_t metaId_ = 50;
size_t storageId_ = 10000;
};
} // namespace hf3fs::tests

51
tests/GtestHelpers.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include <folly/experimental/TestUtil.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <gtest/gtest.h>
#define ASSERT_OK(code_to_run) \
do { \
auto &&_status = code_to_run; \
ASSERT_TRUE(_status) << _status.error().describe(); \
} while (0)
#define ASSERT_RESULT_EQ(result, expr) \
do { \
auto &&_res = (expr); \
ASSERT_OK(_res); \
ASSERT_EQ((result), *_res); \
} while (false)
#define ASSERT_ERROR(code_to_run, expected_code) \
do { \
auto &&_status = code_to_run; \
ASSERT_TRUE(_status.hasError()); \
ASSERT_EQ(_status.error().code(), expected_code) << _status.error().describe(); \
} while (0)
#define CO_ASSERT_OK(code_to_run) \
do { \
auto &&_status = code_to_run; \
CO_ASSERT_FALSE(_status.hasError()) << _status.error().describe(); \
} while (0)
#define CO_ASSERT_ERROR(code_to_run, expected_code) \
do { \
auto &&_status = code_to_run; \
CO_ASSERT_TRUE(_status.hasError()); \
CO_ASSERT_EQ(_status.error().code(), expected_code) << _status.error().describe(); \
} while (0)
#define CO_AWAIT_ASSERT_ERROR(error_code, command) \
do { \
auto _r = co_await (command); \
CO_ASSERT_TRUE(_r.hasError()); \
CO_ASSERT_EQ(_r.error().code(), error_code) << _r.error().describe(); \
} while (0)
#define CO_AWAIT_ASSERT_OK(command) \
do { \
auto _r = co_await (command); \
CO_ASSERT_FALSE(_r.hasError()) << _r.error().describe(); \
} while (0)

View File

@@ -0,0 +1 @@
target_add_test(test_analytics analytics meta common storage-fbs meta-fbs mgmtd-fbs)

View File

@@ -0,0 +1,138 @@
#include <arrow/io/file.h>
#include <folly/logging/xlog.h>
#include <optional>
#include <parquet/schema.h>
#include "analytics/SerdeObjectReader.h"
#include "analytics/SerdeObjectWriter.h"
#include "fbs/core/user/User.h"
#include "fbs/meta/Schema.h"
#include "fbs/storage/Common.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::analytics {
template <serde::SerdeType T>
void readAndCompareSerdeObjectDump(const T &expected) {
{
auto writer = SerdeObjectWriter<T>::open(fmt::format("{}.parquet", nameof::nameof_short_type<T>()));
ASSERT_NE(writer, nullptr);
*writer << expected << parquet::EndRowGroup;
}
auto reader = SerdeObjectReader<T>::open(fmt::format("{}.parquet", nameof::nameof_short_type<T>()));
ASSERT_NE(reader, nullptr);
T fromFile;
*reader >> fromFile;
ASSERT_TRUE(serde::equals(expected, fromFile))
<< "Expected: " << serde::toJsonString(expected, true, true) << std::endl
<< "FromFile: " << serde::toJsonString(fromFile, true, true);
}
TEST(TestSerdeObjectReader, ChunkMeta) {
storage::ChunkMeta chunkmeta{
.chunkId = storage::ChunkId(1, 1),
.updateVer = storage::ChunkVer(1),
.commitVer = storage::ChunkVer(2),
.checksum =
storage::ChecksumInfo{
.type = storage::ChecksumType::CRC32C,
.value = 123,
},
};
readAndCompareSerdeObjectDump(chunkmeta);
}
TEST(TestSerdeObjectReader, Inode) {
meta::Inode inode{
meta::InodeId{0xFF},
meta::InodeData{
.type =
meta::File{
meta::Layout{
.tableId = meta::ChainTableId{1},
.tableVersion = meta::ChainTableVersion{1},
.chains = meta::Layout::ChainRange(101 /*baseIndex*/,
meta::Layout::ChainRange::Shuffle::STD_SHUFFLE_MT19937,
0xFFEE /*seed*/),
},
},
.nlink = 10,
.atime = UtcTime::clock::now(),
.ctime = UtcTime::clock::now(),
.mtime = UtcTime::clock::now(),
},
};
readAndCompareSerdeObjectDump(inode);
}
TEST(TestSerdeObjectReader, DirEntry) {
meta::DirEntry entry{meta::InodeId{0x10},
"direntry",
meta::DirEntryData{meta::InodeId{0xFF},
meta::InodeType::File,
meta::Acl(flat::Uid(1), flat::Gid(1), flat::Permission(0644))}};
readAndCompareSerdeObjectDump(entry);
entry.dirAcl = std::nullopt;
readAndCompareSerdeObjectDump(entry);
}
TEST(TestSerdeObjectReader, IOResult) {
storage::IOResult ioResult{
1024U,
storage::ChunkVer{1},
storage::ChunkVer{2},
storage::ChecksumInfo{storage::ChecksumType::CRC32C, 0xFFEEAABB},
storage::ChainVer{123},
};
readAndCompareSerdeObjectDump(ioResult);
}
TEST(TestSerdeObjectReader, UpdateReq) {
storage::UpdateReq updateReq{
.payload =
{
.offset = 123,
.length = 456,
.chunkSize = 789,
.key =
storage::GlobalKey{
.vChainId =
storage::VersionedChainId{
.chainId = storage::ChainId{10001},
.chainVer = storage::ChainVer{2000},
},
},
// .rdmabuf = net::RDMARemoteBuf{},
.updateVer = storage::ChunkVer{999},
.updateType = storage::UpdateType::WRITE,
.checksum =
storage::ChecksumInfo{
.type = storage::ChecksumType::CRC32C,
.value = 0xFFEEAABB,
},
// .inlinebuf =
// storage::UInt8Vector{
// .data = std::vector<uint8_t>(10, 0xFF),
// },
},
.tag =
storage::MessageTag{
ClientId::random(),
storage::RequestId{101},
storage::UpdateChannel{
.id = storage::ChannelId{1},
.seqnum = storage::ChannelSeqNum{1},
},
},
};
readAndCompareSerdeObjectDump(updateReq);
}
} // namespace hf3fs::analytics

View File

@@ -0,0 +1,115 @@
#include <folly/logging/xlog.h>
#include <parquet/schema.h>
#include <string_view>
#include "analytics/SerdeObjectVisitor.h"
#include "fbs/meta/Schema.h"
#include "fbs/storage/Common.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::analytics {
class DebugObjectVisitor : public BaseObjectVisitor<DebugObjectVisitor> {
public:
template <typename T>
void visit(std::string_view k, T &) = delete;
template <typename T>
requires std::is_arithmetic_v<T>
void visit(std::string_view k, T &) { XLOGF(DBG3, "arithmetic visit({})", k); }
template <typename T>
requires std::is_enum_v<T>
void visit(std::string_view k, T &) { XLOGF(DBG3, "enum visit({})", k); }
template <typename T>
requires std::is_convertible_v<T, std::string_view>
void visit(std::string_view k, T &) { XLOGF(DBG3, "string visit({})", k); }
template <StrongTyped T>
void visit(std::string_view k, T &v) {
XLOGF(DBG3, "strongtyped visit({})", k);
visit<typename T::UnderlyingType>(k, v.toUnderType());
}
template <serde::WithReadableSerdeMethod T>
void visit(std::string_view k, T &v) {
XLOGF(DBG3, "WithReadableSerdeMethod visit({})", k);
auto serialized = serde::SerdeMethod<T>::serdeToReadable(v);
visit(k, serialized);
}
template <serde::WithSerdeMethod T>
void visit(std::string_view k, T &v) {
XLOGF(DBG3, "WithSerdeMethod visit({})", k);
auto serialized = serde::SerdeMethod<T>::serdeTo(v);
visit(k, serialized);
}
template <serde::WithReadableSerdeMemberMethod T>
void visit(std::string_view k, T &v) {
XLOGF(DBG3, "WithReadableSerdeMemberMethod visit({})", k);
auto serialized = v.serdeToReadable();
visit(k, serialized);
}
template <serde::WithSerdeMemberMethod T>
void visit(std::string_view k, T &v) {
XLOGF(DBG3, "WithSerdeMemberMethod visit({})", k);
auto serialized = v.serdeTo();
visit(k, serialized);
}
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
void visit(std::string_view k, T &val) {
XLOGF(DBG3, "serdetype visit({})", k);
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
}
template <typename T>
requires is_variant_v<T>
void visit(std::string_view k, T &val) {
XLOGF(DBG3, "variant visit({})", k);
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
}
template <typename T>
requires is_vector_v<T> || is_set_v<T>
void visit(std::string_view k, T &val) {
XLOGF(DBG3, "container visit({})", k);
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
}
template <typename T>
requires is_optional_v<T>
void visit(std::string_view k, T &val) {
XLOGF(DBG3, "container visit({})", k);
BaseObjectVisitor<DebugObjectVisitor>::visit(k, val);
}
};
TEST(TestSerdeObjectVisitor, ChunkId) {
storage::ChunkId chunkId;
DebugObjectVisitor visitor;
visitor.visit("chunkId", chunkId);
}
TEST(TestSerdeObjectVisitor, ChunkMeta) {
storage::ChunkMeta chunkmeta;
DebugObjectVisitor visitor;
visitor.visit("chunkmeta", chunkmeta);
}
TEST(TestSerdeObjectVisitor, InodeId) {
meta::InodeId inodeId;
DebugObjectVisitor visitor;
visitor.visit("inodeId", inodeId);
}
TEST(TestSerdeObjectVisitor, Inode) {
DebugObjectVisitor visitor;
meta::Inode inode;
visitor.visit("inode", inode);
}
} // namespace hf3fs::analytics

View File

@@ -0,0 +1,40 @@
#include <arrow/io/file.h>
#include <folly/logging/xlog.h>
#include <parquet/schema.h>
#include "analytics/SerdeObjectWriter.h"
#include "fbs/meta/Schema.h"
#include "fbs/storage/Common.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::analytics {
TEST(TestSerdeObjectWriter, ChunkMeta) {
auto writer = SerdeObjectWriter<storage::ChunkMeta>::open("TestSerdeObjectWriter.ChunkMeta.parquet");
ASSERT_NE(writer, nullptr);
storage::ChunkMeta chunkmeta;
*writer << chunkmeta << parquet::EndRowGroup;
}
TEST(TestSerdeObjectWriter, Inode) {
auto writer = SerdeObjectWriter<meta::Inode>::open("TestSerdeObjectWriter.Inode.parquet");
ASSERT_NE(writer, nullptr);
meta::Inode inode;
*writer << inode << parquet::EndRowGroup;
}
TEST(TestSerdeObjectWriter, DirEntry) {
auto writer = SerdeObjectWriter<meta::DirEntry>::open("TestSerdeObjectWriter.DirEntry.parquet");
ASSERT_NE(writer, nullptr);
meta::DirEntry entry;
*writer << entry << parquet::EndRowGroup;
}
TEST(TestSerdeObjectWriter, UpdateReq) {
auto writer = SerdeObjectWriter<storage::UpdateReq>::open("TestSerdeObjectWriter.UpdateReq.parquet");
ASSERT_NE(writer, nullptr);
storage::UpdateReq updateReq;
*writer << updateReq << parquet::EndRowGroup;
}
} // namespace hf3fs::analytics

View File

@@ -0,0 +1,39 @@
#include <folly/logging/xlog.h>
#include <parquet/schema.h>
#include "analytics/SerdeSchemaBuilder.h"
#include "fbs/meta/Schema.h"
#include "fbs/storage/Common.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::analytics {
using namespace parquet;
TEST(TestSerdeSchemaBuilder, ChunkMeta) {
SerdeSchemaBuilder<storage::ChunkMeta> builder;
auto chunkmetaSchema = builder.getSchema();
std::ostringstream outstr;
schema::PrintSchema(chunkmetaSchema.get(), outstr);
XLOGF(INFO, "chunkmeta schema: {}", outstr.str());
}
TEST(TestSerdeSchemaBuilder, Inode) {
SerdeSchemaBuilder<meta::Inode> builder;
auto inodeSchema = builder.getSchema();
std::ostringstream outstr;
schema::PrintSchema(inodeSchema.get(), outstr);
XLOGF(INFO, "inode schema: {}", outstr.str());
}
TEST(TestSerdeSchemaBuilder, DirEntry) {
SerdeSchemaBuilder<meta::DirEntry> builder;
auto dirEntrySchema = builder.getSchema();
std::ostringstream outstr;
schema::PrintSchema(dirEntrySchema.get(), outstr);
XLOGF(INFO, "inode schema: {}", outstr.str());
}
} // namespace hf3fs::analytics

View File

@@ -0,0 +1,139 @@
#include <folly/logging/xlog.h>
#include <parquet/schema.h>
#include "analytics/SerdeStructVisitor.h"
#include "fbs/meta/Schema.h"
#include "fbs/storage/Common.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::analytics {
class DebugStructVisitor : public BaseStructVisitor<DebugStructVisitor> {
public:
// default
template <typename T>
void visit(std::string_view k) = delete;
template <>
void visit<uint16_t>(std::string_view k) {
XLOGF(DBG3, "uint16_t visit({})", k);
}
template <>
void visit<uint32_t>(std::string_view k) {
XLOGF(DBG3, "uint32_t visit({})", k);
}
template <>
void visit<uint64_t>(std::string_view k) {
XLOGF(DBG3, "uint64_t visit({})", k);
}
template <>
void visit<int16_t>(std::string_view k) {
XLOGF(DBG3, "int16_t visit({})", k);
}
template <>
void visit<int32_t>(std::string_view k) {
XLOGF(DBG3, "int32_t visit({})", k);
}
template <>
void visit<int64_t>(std::string_view k) {
XLOGF(DBG3, "int64_t visit({})", k);
}
template <typename T>
requires std::is_enum_v<T>
void visit(std::string_view k) { XLOGF(DBG3, "enum visit({})", k); }
template <serde::ConvertibleToString T>
void visit(std::string_view k) {
XLOGF(DBG3, "string visit({})", k);
}
template <StrongTyped T>
void visit(std::string_view k) {
XLOGF(DBG3, "strongtyped visit({})", k);
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
}
template <serde::SerdeTypeWithoutSpecializedSerdeMethod T>
void visit(std::string_view k) {
XLOGF(DBG3, "serdetype visit({})", k);
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
}
template <serde::WithReadableSerdeMethod T>
void visit(std::string_view k) {
XLOGF(DBG3, "WithReadableSerdeMethod visit({})", k);
visit<serde::SerdeToReadableReturnType<T>>(k);
}
template <serde::WithSerdeMethod T>
void visit(std::string_view k) {
XLOGF(DBG3, "WithSerdeMethod visit({})", k);
visit<serde::SerdeToReturnType<T>>(k);
}
template <serde::WithReadableSerdeMemberMethod T>
void visit(std::string_view k) {
XLOGF(DBG3, "WithReadableSerdeMemberMethod visit({})", k);
visit<serde::SerdeToReadableMemberMethodReturnType<T>>(k);
}
template <serde::WithSerdeMemberMethod T>
void visit(std::string_view k) {
XLOGF(DBG3, "WithSerdeMemberMethod visit({})", k);
visit<serde::SerdeToMemberMethodReturnType<T>>(k);
}
template <typename T>
requires is_variant_v<T>
void visit(std::string_view k) {
XLOGF(DBG3, "variant visit({})", k);
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
}
template <typename T>
requires is_vector_v<T> || is_set_v<T>
void visit(std::string_view k) {
XLOGF(DBG3, "container visit({})", k);
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
}
template <typename T>
requires is_optional_v<T>
void visit(std::string_view k) {
XLOGF(DBG3, "container visit({})", k);
BaseStructVisitor<DebugStructVisitor>::visit<T>(k);
}
};
TEST(TestSerdeStructVisitor, ChunkId) {
DebugStructVisitor visitor;
visitor.visit<storage::ChunkId>("chunkId");
}
TEST(TestSerdeStructVisitor, ChunkMeta) {
DebugStructVisitor visitor;
visitor.visit<storage::ChunkMeta>("chunkmeta");
}
TEST(TestSerdeStructVisitor, InodeId) {
DebugStructVisitor visitor;
visitor.visit<meta::InodeId>("inodeId");
}
TEST(TestSerdeStructVisitor, Inode) {
DebugStructVisitor visitor;
visitor.visit<meta::Inode>("inode");
}
TEST(TestSerdeStructVisitor, DirEntry) {
DebugStructVisitor visitor;
visitor.visit<meta::DirEntry>("dirEntry");
}
} // namespace hf3fs::analytics

View File

@@ -0,0 +1,134 @@
#include <folly/logging/xlog.h>
#include "analytics/StructuredTraceLog.h"
#include "common/monitor/ScopedMetricsWriter.h"
#include "fbs/storage/Common.h"
#include "meta/event/Event.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::analytics {
TEST(TestStructuredTraceLog, Open) {
StructuredTraceLog<storage::StorageEventTrace>::Config config;
config.set_enabled(true);
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
ASSERT_TRUE(traceLog.open());
}
TEST(TestStructuredTraceLog, Close) {
StructuredTraceLog<storage::StorageEventTrace>::Config config;
config.set_enabled(true);
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
ASSERT_TRUE(traceLog.open());
traceLog.close();
}
TEST(TestStructuredTraceLog, MetaEventTrace) {
StructuredTraceLog<meta::server::MetaEventTrace>::Config config;
config.set_enabled(true);
config.set_dump_interval(100_ms);
StructuredTraceLog<meta::server::MetaEventTrace> traceLog(config);
ASSERT_TRUE(traceLog.open());
for (size_t loop = 0; loop < 100'000; loop++) {
traceLog.append(meta::server::MetaEventTrace{
.inodeId = meta::InodeId{loop},
.entryName = std::to_string(loop),
.client = ClientId::zero(),
});
}
traceLog.close();
}
auto createStorageEventTrace(size_t id) {
return storage::StorageEventTrace{
.updateReq =
storage::UpdateReq{
.tag =
storage::MessageTag{
ClientId::zero(),
storage::RequestId{id},
storage::UpdateChannel{
.id = storage::ChannelId{id},
.seqnum = storage::ChannelSeqNum{id},
},
},
},
};
}
TEST(TestStructuredTraceLog, StorageEventTrace) {
StructuredTraceLog<storage::StorageEventTrace>::Config config;
config.set_enabled(true);
config.set_dump_interval(100_ms);
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
ASSERT_TRUE(traceLog.open());
for (size_t loop = 0; loop < 100'000; loop++) {
traceLog.append(createStorageEventTrace(loop));
}
traceLog.close();
}
TEST(TestStructuredTraceLog, MultiThreadsAppend) {
auto concurrency = std::min(std::thread::hardware_concurrency(), 64U);
StructuredTraceLog<storage::StorageEventTrace>::Config config;
config.set_enabled(true);
config.set_max_num_writers(concurrency / 2);
config.set_dump_interval(1_s);
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
ASSERT_TRUE(traceLog.open());
std::vector<std::future<void>> tasks;
for (size_t i = 0; i < concurrency; i++) {
tasks.push_back(std::async(std::launch::async, [&traceLog]() {
for (size_t loop = 0; loop < 100'000; loop++) {
traceLog.append(createStorageEventTrace(loop));
}
}));
}
for (auto &t : tasks) t.wait();
traceLog.close();
}
TEST(TestStructuredTraceLog, MultiThreadsNewEntry) {
auto concurrency = std::min(std::thread::hardware_concurrency(), 64U);
StructuredTraceLog<storage::StorageEventTrace>::Config config;
config.set_enabled(true);
config.set_max_num_writers(concurrency);
config.set_dump_interval(1_s);
StructuredTraceLog<storage::StorageEventTrace> traceLog(config);
ASSERT_TRUE(traceLog.open());
std::vector<std::future<void>> tasks;
monitor::LatencyRecorder latRecorder("TestStructuredTraceLog");
for (size_t i = 0; i < concurrency; i++) {
tasks.push_back(std::async(std::launch::async, [&traceLog, &latRecorder]() {
for (size_t loop = 0; loop < 100; loop++) {
auto entry = traceLog.newEntry(createStorageEventTrace(loop));
entry.reset();
}
for (size_t loop = 0; loop < 100'000; loop++) {
monitor::ScopedLatencyWriter latWriter(latRecorder);
auto entry = traceLog.newEntry(createStorageEventTrace(loop));
entry.reset();
}
}));
}
for (auto &t : tasks) t.wait();
traceLog.close();
std::vector<monitor::Sample> samples;
latRecorder.collect(samples);
for (const auto &s : samples) {
XLOGF(WARN, "latency: {}", s.dist());
}
}
} // namespace hf3fs::analytics

View File

@@ -0,0 +1,2 @@
target_add_test(test_client hf3fs_api storage-client meta-client meta mgmtd)

View File

@@ -0,0 +1,37 @@
#pragma once
#include "client/mgmtd/MgmtdClientForServer.h"
#include "stubs/common/RealStubFactory.h"
#include "stubs/mgmtd/MgmtdServiceStub.h"
namespace hf3fs::client {
struct MgmtdClientWithConfig {
MgmtdClient::Config config;
String clusterId;
stubs::ClientContextCreator clientContextCreator;
std::shared_ptr<MgmtdClient> rawClient;
std::unique_ptr<MgmtdClientForServer> client;
MgmtdClientWithConfig(String clusterId,
stubs::ClientContextCreator clientContextCreator,
std::vector<net::Address> mgmtdServerAddrs) {
config.set_mgmtd_server_addresses(mgmtdServerAddrs);
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
config.set_enable_auto_extend_client_session(false);
this->clusterId = std::move(clusterId);
this->clientContextCreator = std::move(clientContextCreator);
auto stubFactory = std::make_unique<stubs::RealStubFactory<mgmtd::MgmtdServiceStub>>(this->clientContextCreator);
rawClient = std::make_unique<MgmtdClient>(this->clusterId, std::move(stubFactory), config);
client = std::make_unique<MgmtdClientForServer>(rawClient);
}
MgmtdClientForServer *operator->() const { return client.get(); }
MgmtdClientForServer &operator*() const { return *client; }
};
} // namespace hf3fs::client

View File

@@ -0,0 +1,88 @@
#pragma once
#include "client/mgmtd/MgmtdClientForServer.h"
#include "common/kv/mem/MemKVEngine.h"
#include "meta/service/MetaServer.h"
#include "mgmtd/MgmtdServer.h"
namespace hf3fs {
template <typename ServerT>
struct ServerWithConfig {
typename ServerT::Config config;
String clusterId;
flat::NodeId nodeId;
std::unique_ptr<ServerT> server;
String hostname;
uint32_t pid = 1;
ServerWithConfig(String clusterId, flat::NodeId nodeId) {
this->clusterId = std::move(clusterId);
this->nodeId = nodeId;
auto &baseConfig = config.base();
for (size_t i = 0; i < baseConfig.groups_length(); ++i) {
baseConfig.groups(i).set_network_type(net::Address::LOCAL);
baseConfig.groups(i).listener().set_listen_port(0);
baseConfig.groups(i).listener().set_reuse_port(true);
}
}
Result<Void> start(std::shared_ptr<kv::IKVEngine> kvEngine) {
assert(!server);
auto server = std::make_unique<ServerT>(config);
RETURN_ON_ERROR(server->setup());
flat::AppInfo appInfo;
appInfo.nodeId = nodeId;
appInfo.clusterId = clusterId;
appInfo.hostname = fmt::format("hostname.{}", nodeId.toUnderType());
appInfo.pid = pid++;
std::vector<net::Address> serverAddrs;
for (auto &group : server->groups()) {
appInfo.serviceGroups.emplace_back(group->serviceNameList(), group->addressList());
serverAddrs.insert(serverAddrs.end(), group->addressList().begin(), group->addressList().end());
}
RETURN_ON_ERROR(server->start(appInfo, std::move(kvEngine)));
this->server = std::move(server);
return Void{};
}
void stop() {
if (server) server->stopAndJoin();
server.reset();
}
Result<Void> restart(std::shared_ptr<kv::IKVEngine> kvEngine) {
stop();
return start(std::move(kvEngine));
}
std::vector<net::Address> collectAddressList(std::string_view serviceName) const {
std::vector<net::Address> serverAddrs;
for (const auto &group : server->groups()) {
const auto &names = group->serviceNameList();
const auto &addrs = group->addressList();
if (std::find(names.begin(), names.end(), serviceName) != names.end()) {
serverAddrs.insert(serverAddrs.end(), addrs.begin(), addrs.end());
}
}
return serverAddrs;
}
};
using MgmtdServerWithConfig = ServerWithConfig<mgmtd::MgmtdServer>;
struct MetaServerWithConfig : ServerWithConfig<meta::server::MetaServer> {
using Base = ServerWithConfig<meta::server::MetaServer>;
MetaServerWithConfig(String clusterId, flat::NodeId nodeId, std::vector<net::Address> mgmtdServerAddrs)
: Base(std::move(clusterId), nodeId) {
auto &mgmtdClientConfig = config.mgmtd_client();
mgmtdClientConfig.set_mgmtd_server_addresses(mgmtdServerAddrs);
}
};
} // namespace hf3fs

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,615 @@
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Sleep.h>
#include "client/mgmtd/MgmtdClient.h"
#include "fbs/mgmtd/NodeConversion.h"
#include "tests/GtestHelpers.h"
#include "tests/stubs/DummyMgmtdServiceStub.h"
namespace hf3fs::client::tests {
namespace {
class MgmtdClientTest : public ::testing::Test {
protected:
MgmtdClientTest() {}
};
#define CO_START_CLIENT(client) \
co_await (client).start(); \
co_await folly::coro::co_scope_exit([&]() -> CoTask<void> { co_await (client).stop(); })
std::vector<std::pair<String, String>> convert(const std::vector<std::pair<String, net::Address>> &v) {
std::vector<std::pair<String, String>> ret;
for (const auto &[method, addr] : v) {
ret.emplace_back(method, addr.toString());
}
std::sort(ret.begin(), ret.end());
return ret;
}
#define CO_ASSERT_RECORDS_EQ(expectedRecords) \
do { \
auto _expected = convert(expectedRecords); \
auto _actual = convert(Machine::visitRecords); \
CO_ASSERT_EQ(_actual, _expected); \
} while (false)
flat::PersistentNodeInfo makePrimary(flat::NodeId id, net::Address addr) {
flat::PersistentNodeInfo info;
info.nodeId = id;
flat::ServiceGroupInfo sgi;
sgi.services.insert("Mgmtd");
sgi.endpoints.push_back(addr);
info.serviceGroups.push_back(sgi);
return info;
}
struct Machine {
Machine(flat::NodeId id, std::vector<net::Address> addrs) {
selfInfo = makePrimary(id, addrs[0]);
for (size_t i = 1; i < addrs.size(); ++i) {
selfInfo.serviceGroups[0].endpoints.push_back(addrs[i]);
}
}
std::unique_ptr<mgmtd::DummyMgmtdServiceStub> createStub(net::Address addr) {
const auto &endpoints = selfInfo.serviceGroups[0].endpoints;
auto it = std::find(endpoints.begin(), endpoints.end(), addr);
if (it == endpoints.end()) return nullptr;
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
stub->set_getPrimaryMgmtdFunc([this, addr](const mgmtd::GetPrimaryMgmtdReq &) -> Result<mgmtd::GetPrimaryMgmtdRsp> {
visitRecords.emplace_back("GetPrimaryMgmtd", addr);
if (primary.hasError())
return makeError(primary.error());
else if (*primary)
return mgmtd::GetPrimaryMgmtdRsp::create(**primary);
return mgmtd::GetPrimaryMgmtdRsp::create(std::nullopt);
})
.set_getRoutingInfoFunc([this, addr](const mgmtd::GetRoutingInfoReq &) -> Result<mgmtd::GetRoutingInfoRsp> {
visitRecords.emplace_back("GetRoutingInfo", addr);
if (primary.hasError()) return makeError(primary.error());
if (!*primary) return makeError(MgmtdCode::kNotPrimary);
if (**primary != selfInfo) return makeError(MgmtdCode::kNotPrimary, fmt::format("{}", (*primary)->nodeId));
if (routingInfo.hasError()) return makeError(routingInfo.error());
if (*routingInfo) {
return mgmtd::GetRoutingInfoRsp::create(**routingInfo);
}
return mgmtd::GetRoutingInfoRsp::create(std::nullopt);
});
return stub;
}
flat::PersistentNodeInfo selfInfo;
Result<flat::PersistentNodeInfo *> primary = makeError(RPCCode::kConnectFailed);
Result<flat::RoutingInfo *> routingInfo = makeError(RPCCode::kConnectFailed);
static std::vector<std::pair<String, net::Address>> visitRecords;
};
std::vector<std::pair<String, net::Address>> Machine::visitRecords;
TEST_F(MgmtdClientTest, testStartStopWithWrongConfig) {
MgmtdClient::Config config;
config.set_auto_refresh_interval(1_ms);
config.set_auto_heartbeat_interval(1_ms);
MgmtdClient client("", nullptr, config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
co_await folly::coro::sleep(std::chrono::milliseconds(100));
}());
}
TEST_F(MgmtdClientTest, testRepetitiveStartStop) {
MgmtdClient::Config config;
config.set_auto_refresh_interval(1_ms);
config.set_auto_heartbeat_interval(1_ms);
MgmtdClient client("", nullptr, config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
co_await folly::coro::sleep(std::chrono::milliseconds(10));
co_await client.stop();
co_await client.start();
co_await folly::coro::sleep(std::chrono::milliseconds(10));
co_await client.stop();
co_await client.start();
co_await folly::coro::sleep(std::chrono::milliseconds(10));
co_await client.stop();
}());
}
TEST_F(MgmtdClientTest, testGetRoutingInfoBeforeStart) {
MgmtdClient::Config config;
MgmtdClient client("", nullptr, config);
auto info = client.getRoutingInfo();
ASSERT_TRUE(!info);
}
net::Address ada = net::Address::from("127.0.0.1:8080").value();
net::Address adb = net::Address::from("192.168.0.1:9000").value();
net::Address adc = net::Address::from("192.168.0.2:8000").value();
net::Address add = net::Address::from("192.168.0.3:8800").value();
net::Address ade; // invalid addr
net::Address adf = net::Address::from("RDMA://127.0.0.1:8080").value();
net::Address adg = net::Address::from("RDMA://192.168.0.1:9000").value();
net::Address adh = net::Address::from("RDMA://192.168.0.2:8000").value();
net::Address adi = net::Address::from("RDMA://192.168.0.3:8800").value();
net::Address adj = net::Address::from("192.168.0.4:8888").value();
TEST_F(MgmtdClientTest, testWhenNoPrimary) {
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
return std::make_unique<mgmtd::DummyMgmtdServiceStub>();
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({ada, adb, adc});
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
MgmtdClient client("", std::make_unique<StubFactory>(), config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
CO_ASSERT_TRUE(!client.getRoutingInfo());
}());
}
TEST_F(MgmtdClientTest, testInvalidAddress) {
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({ada, ade});
MgmtdClient client("", nullptr, config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), StatusCode::kInvalidConfig);
}());
}
TEST_F(MgmtdClientTest, testAddressTypeMismatch) {
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({ada, adf});
config.set_network_type(net::Address::TCP);
MgmtdClient client("", nullptr, config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), StatusCode::kInvalidConfig);
}());
}
TEST_F(MgmtdClientTest, testWhenLastIsPrimary) {
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
if (addr == adc) {
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), adc));
})
.set_getRoutingInfoFunc(
[](const mgmtd::GetRoutingInfoReq &) { return mgmtd::GetRoutingInfoRsp::create(std::nullopt); });
}
return stub;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({ada, adb, adc});
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
MgmtdClient client("", std::make_unique<StubFactory>(), config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
auto ri = client.getRoutingInfo();
CO_ASSERT_TRUE(ri);
CO_ASSERT_TRUE(!ri->raw());
}());
}
TEST_F(MgmtdClientTest, testWhenPrimaryNotInConfig) {
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
if (addr == adc) {
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), add));
});
} else if (addr == add) {
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), add));
})
.set_getRoutingInfoFunc(
[](const mgmtd::GetRoutingInfoReq &) { return mgmtd::GetRoutingInfoRsp::create(std::nullopt); });
}
return stub;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({ada, adb, adc});
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
MgmtdClient client("", std::make_unique<StubFactory>(), config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
auto ri = client.getRoutingInfo();
CO_ASSERT_TRUE(ri);
CO_ASSERT_TRUE(!ri->raw());
}());
}
TEST_F(MgmtdClientTest, testWhenGetPrimaryLoop) {
std::vector<net::Address> addresses = {ada, adb, adc};
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::vector<net::Address> addresses;
explicit StubFactory(std::vector<net::Address> v)
: addresses(std::move(v)) {}
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
for (size_t i = 0; i < addresses.size(); ++i) {
if (addr == addresses[i]) {
if (i + 1 == addresses.size()) {
stub->set_getPrimaryMgmtdFunc([this](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), addresses[0]));
});
} else {
stub->set_getPrimaryMgmtdFunc([this, i](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(i + 2), addresses[i + 1]));
});
}
break;
}
}
return stub;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses(addresses);
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
MgmtdClient client("", std::make_unique<StubFactory>(addresses), config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
CO_ASSERT_TRUE(!client.getRoutingInfo());
}());
}
TEST_F(MgmtdClientTest, testRetryOnRefreshFail) {
std::vector<net::Address> addresses = {ada, adb, adc};
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
int counta = 0;
int countb = 0;
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
if (addr == ada) {
stub->set_getPrimaryMgmtdFunc([this](const mgmtd::GetPrimaryMgmtdReq &) -> Result<mgmtd::GetPrimaryMgmtdRsp> {
if (++counta == 1) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
} else {
return makeError(RPCCode::kConnectFailed);
}
})
.set_getRoutingInfoFunc([this](const mgmtd::GetRoutingInfoReq &) -> Result<mgmtd::GetRoutingInfoRsp> {
if (++countb == 1)
return mgmtd::GetRoutingInfoRsp::create(std::nullopt);
else
return makeError(RPCCode::kConnectFailed);
});
} else if (addr == adb) {
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(2), adb));
})
.set_getRoutingInfoFunc([](const mgmtd::GetRoutingInfoReq &) {
flat::RoutingInfo ri;
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
return mgmtd::GetRoutingInfoRsp::create(ri);
});
}
return stub;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses(addresses);
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
MgmtdClient client("", std::make_unique<StubFactory>(), config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
auto ri = client.getRoutingInfo();
CO_ASSERT_TRUE(ri && !ri->raw());
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
ri = client.getRoutingInfo();
CO_ASSERT_TRUE(ri && ri->raw());
CO_ASSERT_EQ(ri->raw()->routingInfoVersion, flat::RoutingInfoVersion(2));
}());
}
TEST_F(MgmtdClientTest, testRefreshRoutingInfoCallback) {
std::vector<net::Address> addresses = {ada};
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
stub->set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
})
.set_getRoutingInfoFunc([](const mgmtd::GetRoutingInfoReq &req) {
flat::RoutingInfo ri;
ri.routingInfoVersion = flat::RoutingInfoVersion(req.routingInfoVersion + 1);
return mgmtd::GetRoutingInfoRsp::create(ri);
});
return stub;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses(addresses);
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
MgmtdClient client("", std::make_unique<StubFactory>(), config);
std::vector<std::shared_ptr<RoutingInfo>> ris;
client.addRoutingInfoListener("test", [&](std::shared_ptr<RoutingInfo> ri) { ris.push_back(std::move(ri)); });
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
for (int i = 0; i < 10; ++i) CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
}());
ASSERT_EQ(ris.size(), 10);
for (int i = 0; i < 10; ++i) {
const auto &ri = ris[i];
ASSERT_TRUE(ri && ri->raw());
ASSERT_EQ(ri->raw()->routingInfoVersion, flat::RoutingInfoVersion(i + 1));
if (i > 0) ASSERT_TRUE(ri->lastRefreshTime() > ris[i - 1]->lastRefreshTime());
}
}
TEST_F(MgmtdClientTest, testRetryAllAvailableAddresses) {
Machine::visitRecords.clear();
Machine ma(flat::NodeId(1), {ada, adb, adc});
Machine mb(flat::NodeId(2), {add, adf});
Machine mc(flat::NodeId(3), {adj});
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::vector<Machine *> machines;
explicit StubFactory(std::vector<Machine *> v)
: machines(std::move(v)) {}
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
for (auto *m : machines) {
auto stub = m->createStub(addr);
if (stub) return stub;
}
return nullptr;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({adj});
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
config.set_network_type(net::Address::TCP);
MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);
flat::RoutingInfo ri;
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
ri.nodes[mb.selfInfo.nodeId] = flat::toNode(mb.selfInfo);
ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);
folly::coro::blockingWait([&]() -> CoTask<void> {
mc.primary = &mc.selfInfo;
mc.routingInfo = &ri;
CO_START_CLIENT(client);
Machine::visitRecords.clear();
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
{
std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
CO_ASSERT_RECORDS_EQ(expected);
}
mc.primary = makeError(RPCCode::kConnectFailed);
Machine::visitRecords.clear();
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
{
std::vector<std::pair<String, net::Address>> expected = {
{"GetRoutingInfo", adj},
{"GetPrimaryMgmtd", ada},
{"GetPrimaryMgmtd", adb},
{"GetPrimaryMgmtd", adc},
{"GetPrimaryMgmtd", add},
};
CO_ASSERT_RECORDS_EQ(expected);
}
}());
}
TEST_F(MgmtdClientTest, testRetryEndWhenNoPrimary) {
Machine::visitRecords.clear();
Machine ma(flat::NodeId(1), {ada, adb, adc});
Machine mb(flat::NodeId(2), {add, adf});
Machine mc(flat::NodeId(3), {adj});
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::vector<Machine *> machines;
explicit StubFactory(std::vector<Machine *> v)
: machines(std::move(v)) {}
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
for (auto *m : machines) {
auto stub = m->createStub(addr);
if (stub) return stub;
}
return nullptr;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({adj});
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
config.set_network_type(net::Address::TCP);
MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);
flat::RoutingInfo ri;
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
ri.nodes[mb.selfInfo.nodeId] = flat::toNode(mb.selfInfo);
ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);
folly::coro::blockingWait([&]() -> CoTask<void> {
mc.primary = &mc.selfInfo;
mc.routingInfo = &ri;
CO_START_CLIENT(client);
Machine::visitRecords.clear();
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
{
std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
CO_ASSERT_RECORDS_EQ(expected);
}
ma.primary = nullptr;
mc.primary = makeError(RPCCode::kConnectFailed);
Machine::visitRecords.clear();
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
{
std::vector<std::pair<String, net::Address>> expected = {
{"GetRoutingInfo", adj},
{"GetPrimaryMgmtd", ada},
};
CO_ASSERT_RECORDS_EQ(expected);
}
}());
}
TEST_F(MgmtdClientTest, testRetryUnknownAddrs) {
Machine::visitRecords.clear();
Machine ma(flat::NodeId(1), {ada, adb, adc});
Machine mb(flat::NodeId(2), {add, adf});
Machine mc(flat::NodeId(3), {adj});
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::vector<Machine *> machines;
explicit StubFactory(std::vector<Machine *> v)
: machines(std::move(v)) {}
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address addr) {
for (auto *m : machines) {
auto stub = m->createStub(addr);
if (stub) return stub;
}
return nullptr;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses({adj, add});
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
config.set_network_type(net::Address::TCP);
MgmtdClient client("", std::make_unique<StubFactory>(std::vector{&ma, &mb, &mc}), config);
flat::RoutingInfo ri;
ri.routingInfoVersion = flat::RoutingInfoVersion(2);
ri.nodes[ma.selfInfo.nodeId] = flat::toNode(ma.selfInfo);
ri.nodes[mc.selfInfo.nodeId] = flat::toNode(mc.selfInfo);
folly::coro::blockingWait([&]() -> CoTask<void> {
mc.primary = &mc.selfInfo;
mc.routingInfo = &ri;
CO_START_CLIENT(client);
Machine::visitRecords.clear();
CO_ASSERT_OK(co_await client.refreshRoutingInfo(false));
{
std::vector<std::pair<String, net::Address>> expected = {{"GetPrimaryMgmtd", adj}, {"GetRoutingInfo", adj}};
CO_ASSERT_RECORDS_EQ(expected);
}
mc.primary = makeError(RPCCode::kConnectFailed);
Machine::visitRecords.clear();
CO_ASSERT_ERROR(co_await client.refreshRoutingInfo(false), MgmtdClientCode::kPrimaryMgmtdNotFound);
{
std::vector<std::pair<String, net::Address>> expected = {
{"GetRoutingInfo", adj},
{"GetPrimaryMgmtd", ada},
{"GetPrimaryMgmtd", adb},
{"GetPrimaryMgmtd", adc},
{"GetPrimaryMgmtd", add},
};
CO_ASSERT_RECORDS_EQ(expected);
}
}());
}
TEST_F(MgmtdClientTest, testSetGetConfigViaInvoke) {
std::vector<net::Address> addresses = {ada};
struct StubFactory : public stubs::IStubFactory<mgmtd::IMgmtdServiceStub> {
std::map<flat::NodeType, flat::ConfigInfo> configs;
std::unique_ptr<mgmtd::IMgmtdServiceStub> create(net::Address) {
auto stub = std::make_unique<mgmtd::DummyMgmtdServiceStub>();
stub->set_setConfigFunc([this](const mgmtd::SetConfigReq &req) -> Result<mgmtd::SetConfigRsp> {
auto &info = configs[req.nodeType];
auto cv = flat::ConfigVersion(info.configVersion + 1);
info = flat::ConfigInfo::create(cv, req.content, req.desc);
return mgmtd::SetConfigRsp::create(cv);
})
.set_getConfigFunc([this](const mgmtd::GetConfigReq &req) -> Result<mgmtd::GetConfigRsp> {
if (configs.contains(req.nodeType)) return mgmtd::GetConfigRsp::create(configs[req.nodeType]);
return mgmtd::GetConfigRsp::create(std::nullopt);
})
.set_getPrimaryMgmtdFunc([](const mgmtd::GetPrimaryMgmtdReq &) {
return mgmtd::GetPrimaryMgmtdRsp::create(makePrimary(flat::NodeId(1), ada));
});
return stub;
}
};
MgmtdClient::Config config;
config.set_mgmtd_server_addresses(addresses);
config.set_enable_auto_refresh(false);
config.set_enable_auto_heartbeat(false);
MgmtdClient client("", std::make_unique<StubFactory>(), config);
folly::coro::blockingWait([&]() -> CoTask<void> {
CO_START_CLIENT(client);
{
auto res = co_await client.setConfig(flat::UserInfo{}, flat::NodeType::MGMTD, String("abcd"), "desc");
CO_ASSERT_OK(res);
}
{
auto res = co_await client.getConfig(flat::NodeType::MGMTD, flat::ConfigVersion{0});
CO_ASSERT_OK(res);
CO_ASSERT_TRUE(res->has_value());
CO_ASSERT_EQ(res->value().configVersion, flat::ConfigVersion{1});
CO_ASSERT_EQ(res->value().content, "abcd");
CO_ASSERT_EQ(res->value().desc, "desc");
res = co_await client.getConfig(flat::NodeType::META, flat::ConfigVersion{0});
CO_ASSERT_OK(res);
CO_ASSERT_TRUE(!res->has_value());
}
}());
}
} // namespace
} // namespace hf3fs::client::tests

View File

@@ -0,0 +1,99 @@
#include <chrono>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Sleep.h>
#include "common/kv/mem/MemKVEngine.h"
#include "meta/service/MetaServer.h"
#include "mgmtd/MgmtdServer.h"
#include "stubs/common/RealStubFactory.h"
#include "stubs/mgmtd/MgmtdServiceStub.h"
#include "tests/GtestHelpers.h"
#include "tests/client/ClientWithConfig.h"
#include "tests/client/ServerWithConfig.h"
namespace hf3fs::client::tests {
namespace {
class MgmtdClusterTest : public ::testing::Test {
protected:
MgmtdClusterTest() = default;
};
#define CO_START_CLIENT(client) \
co_await (client).start(); \
co_await folly::coro::co_scope_exit([&]() -> CoTask<void> { co_await (client).stop(); })
struct ClientWithConfig {
net::Client::Config config;
std::unique_ptr<net::Client> client;
Result<Void> init() {
client = std::make_unique<net::Client>(config);
return client->start("test");
}
};
TEST_F(MgmtdClusterTest, testMgmtdServerRestart) {
const String clusterId = "cluster";
auto kvEngine = std::make_shared<kv::MemKVEngine>();
ClientWithConfig client;
ASSERT_OK(client.init());
MgmtdServerWithConfig mgmtdServer(clusterId, flat::NodeId(1));
ASSERT_OK(mgmtdServer.start(kvEngine));
MgmtdClientWithConfig mgmtdClient(
clusterId,
[&client](net::Address addr) { return client.client->serdeCtx(addr); },
mgmtdServer.collectAddressList("Mgmtd"));
folly::coro::blockingWait([&]() -> CoTask<void> {
co_await folly::coro::sleep(std::chrono::milliseconds(500));
CO_START_CLIENT(*mgmtdClient.client);
// set chain table
flat::ChainTableId tableId{1};
std::vector<flat::ChainSetting> chains;
std::vector<flat::ChainId> chainIds;
{
for (int i = 0, t = 0; i < 10; ++i) {
flat::ChainSetting chain;
chain.chainId = flat::ChainId((tableId << 16) + i + 1);
for (int j = 0; j < 3; ++j) {
flat::ChainTargetSetting target;
target.targetId = flat::TargetId(++t);
chain.targets.push_back(target);
}
chainIds.push_back(chain.chainId);
chains.push_back(std::move(chain));
}
CO_ASSERT_OK(co_await mgmtdClient.rawClient->setChains(flat::UserInfo{}, chains));
CO_ASSERT_OK(co_await mgmtdClient.rawClient->setChainTable(flat::UserInfo{}, tableId, chainIds, ""));
CO_AWAIT_ASSERT_OK(mgmtdClient->refreshRoutingInfo(false));
auto ri = mgmtdClient->getRoutingInfo();
CO_ASSERT_TRUE(ri && ri->raw());
for (const auto &c : chains) {
auto ci = ri->getChain(c.chainId);
CO_ASSERT_TRUE(ci);
CO_ASSERT_EQ(ci->chainId, c.chainId);
}
}
// restart mgmtd, expect chain table exists
{
CO_ASSERT_OK(mgmtdServer.restart(kvEngine));
auto ri = mgmtdClient->getRoutingInfo();
CO_ASSERT_TRUE(ri && ri->raw());
for (const auto &c : chains) {
auto ci = ri->getChain(c.chainId);
CO_ASSERT_TRUE(ci);
CO_ASSERT_EQ(ci->chainId, c.chainId);
}
}
}());
}
} // namespace
} // namespace hf3fs::client::tests

View File

@@ -0,0 +1 @@
target_add_test(test_common common fdb mgmtd-fbs)

View File

@@ -0,0 +1,274 @@
#include <folly/experimental/coro/BlockingWait.h>
#include <gtest/gtest.h>
#include <string_view>
#include "common/kv/IKVEngine.h"
#include "common/kv/WithTransaction.h"
#include "common/utils/Coroutine.h"
#include "common/utils/DefaultRetryStrategy.h"
#include "common/utils/Result.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::tests {
namespace {
using namespace ::hf3fs::kv;
using namespace std::chrono_literals;
struct MockSleeper {
CoTask<void> operator()(std::chrono::milliseconds d) {
records.push_back(d);
co_return;
}
std::vector<std::chrono::milliseconds> records;
};
DefaultRetryStrategy<MockSleeper> getDefaultRetryStrategy() {
return DefaultRetryStrategy<MockSleeper>(RetryConfig{10ms, 100ms, 5});
}
struct OpResultSeq : public std::vector<uint32_t> {
using Base = std::vector<uint32_t>;
using Base::Base;
size_t cur_ = 0;
explicit OpResultSeq(Base req)
: Base(std::move(req)) {}
CoTryTask<void> next() {
auto code = at(cur_++);
if (code == StatusCode::kOK) {
co_return Void();
}
co_return makeError(code);
}
std::string_view peak() { return StatusCode::toString(at(cur_)); }
};
class MockROTxn : public IReadOnlyTransaction {
public:
CoTryTask<std::optional<String>> snapshotGet(std::string_view) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<GetRangeResult> snapshotGetRange(const KeySelector &, const KeySelector &, int32_t) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<GetRangeResult> getRange(const KeySelector &, const KeySelector &, int32_t) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<void> cancel() override { co_return makeError(StatusCode::kNotImplemented); }
void reset() override {}
void setReadVersion(int64_t) override {}
};
class MockRWTxn : public IReadWriteTransaction {
public:
MockRWTxn(OpResultSeq &commitSeq)
: commitSeq_(commitSeq) {}
CoTryTask<std::optional<String>> snapshotGet(std::string_view) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<GetRangeResult> snapshotGetRange(const KeySelector &, const KeySelector &, int32_t) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<std::optional<String>> get(std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
CoTryTask<GetRangeResult> getRange(const KeySelector &, const KeySelector &, int32_t) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<void> addReadConflict(std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
CoTryTask<void> addReadConflictRange(std::string_view, std::string_view) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<void> set(std::string_view, std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
CoTryTask<void> clear(std::string_view) override { co_return makeError(StatusCode::kNotImplemented); }
CoTryTask<void> setVersionstampedKey(std::string_view key, uint32_t offset, std::string_view value) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<void> setVersionstampedValue(std::string_view key, std::string_view value, uint32_t offset) override {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<void> commit() override { co_return co_await commitSeq_.next(); }
CoTryTask<void> cancel() override { co_return makeError(StatusCode::kNotImplemented); }
int64_t getCommittedVersion() override { return -1; }
void setReadVersion(int64_t) override {}
void reset() override {}
private:
OpResultSeq &commitSeq_;
};
struct MockHandler {
explicit MockHandler(OpResultSeq &seq)
: seq_(seq) {}
CoTryTask<int> operator()(IReadOnlyTransaction &) {
auto result = co_await seq_.next();
CO_RETURN_ON_ERROR(result);
co_return 10;
}
CoTryTask<void> operator()(IReadWriteTransaction &) {
auto result = co_await seq_.next();
CO_RETURN_ON_ERROR(result);
co_return Void();
}
OpResultSeq &seq_;
};
struct RetryStrategyRef {
DefaultRetryStrategy<MockSleeper> &ref;
template <typename Txn>
Result<Void> init(Txn *txn) {
return ref.init(txn);
}
template <typename Txn>
CoTryTask<void> onError(Txn *txn, Status error) {
co_return co_await ref.onError(txn, error);
}
};
CoTask<std::pair<Result<int>, DefaultRetryStrategy<MockSleeper>>> handleROTxn(OpResultSeq &opSeq) {
auto handler = MockHandler(opSeq);
auto retryStrategy = getDefaultRetryStrategy();
MockROTxn txn;
auto result = co_await WithTransaction<RetryStrategyRef>({retryStrategy}).run(txn, handler);
co_return std::make_pair<Result<int>, DefaultRetryStrategy<MockSleeper>>(std::move(result), std::move(retryStrategy));
}
CoTask<std::pair<Result<Void>, DefaultRetryStrategy<MockSleeper>>> handleRWTxn(OpResultSeq &opSeq,
OpResultSeq &commitSeq) {
auto handler = MockHandler(opSeq);
auto retryStrategy = getDefaultRetryStrategy();
MockRWTxn txn(commitSeq);
auto result = co_await WithTransaction<RetryStrategyRef>({retryStrategy}).run(txn, handler);
co_return std::make_pair<Result<Void>, DefaultRetryStrategy<MockSleeper>>(std::move(result),
std::move(retryStrategy));
}
TEST(WithTransaction, testReadOnly) {
folly::coro::blockingWait([&]() -> CoTask<void> {
{
// normal succeed
OpResultSeq opSeq = {StatusCode::kOK};
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op retry once
OpResultSeq opSeq = {TransactionCode::kThrottled, StatusCode::kOK};
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {10ms};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op fail, cancel fail (IReadOnlyTransaction allows to ignore cancel fail), op fail, cancel fail again, op ok
OpResultSeq opSeq = {TransactionCode::kThrottled, TransactionCode::kTooOld, StatusCode::kOK};
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op could fail at most 5 times then succeed
OpResultSeq opSeq(5, TransactionCode::kThrottled);
opSeq.push_back(StatusCode::kOK);
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op failed 6 times and return error
OpResultSeq opSeq(6, TransactionCode::kThrottled);
opSeq.push_back(StatusCode::kOK);
auto [result, retryStrategy] = co_await handleROTxn(opSeq);
CO_ASSERT_TRUE(result.hasError());
CO_ASSERT_EQ(result.error().code(), TransactionCode::kThrottled);
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
}());
}
TEST(WithTransaction, testReadWrite) {
folly::coro::blockingWait([&]() -> CoTask<void> {
{
// normal succeed
OpResultSeq seq = {StatusCode::kOK};
OpResultSeq txnCommitSeq = {StatusCode::kOK};
auto [result, retryStrategy] = co_await handleRWTxn(seq, txnCommitSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op retry once
OpResultSeq opSeq = {TransactionCode::kThrottled, StatusCode::kOK};
OpResultSeq txnCommitSeq = {StatusCode::kOK};
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {10ms};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op succeed, commit fail, retry op and commit succeed
OpResultSeq opSeq = {StatusCode::kOK, StatusCode::kOK};
OpResultSeq txnCommitSeq = {TransactionCode::kThrottled, StatusCode::kOK};
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {10ms};
std::cout << retryStrategy.getSleeper().records.at(0).count() << std::endl;
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op could fail at most 5 times then succeed
OpResultSeq opSeq(5, TransactionCode::kThrottled);
opSeq.push_back(StatusCode::kOK);
OpResultSeq txnCommitSeq = {StatusCode::kOK};
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
CO_ASSERT_OK(result);
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
{
// op failed 6 times and return error
OpResultSeq opSeq(6, TransactionCode::kThrottled);
opSeq.push_back(StatusCode::kOK);
OpResultSeq txnCommitSeq;
auto [result, retryStrategy] = co_await handleRWTxn(opSeq, txnCommitSeq);
CO_ASSERT_TRUE(result.hasError());
CO_ASSERT_EQ(result.error().code(), TransactionCode::kThrottled);
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
CO_ASSERT_EQ(retryStrategy.getSleeper().records, expected);
}
}());
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,58 @@
#include <atomic>
#include <chrono>
#include <folly/Random.h>
#include <folly/concurrency/ConcurrentHashMap.h>
#include <folly/portability/GTest.h>
#include <functional>
#include <thread>
namespace hf3fs::test {
namespace {
struct Dummy {
std::array<uint8_t, 64u> x;
};
TEST(TestConcurrentHashMap, Normal) {
constexpr auto N = 1000000;
std::atomic<bool> stop = false;
std::atomic<uint64_t> readTimes = 0;
std::atomic<uint64_t> writeTimes = 0;
folly::ConcurrentHashMap<uint64_t, Dummy> map;
for (auto i = 0; i < N; ++i) {
map.insert_or_assign(i, Dummy{});
}
auto start = std::chrono::steady_clock::now();
std::jthread writer([&] {
while (!stop) {
map.insert_or_assign(folly::Random::rand32() % N, Dummy{});
++writeTimes;
}
});
std::vector<std::jthread> readers(4);
for (auto &reader : readers) {
reader = std::jthread([&] {
while (!stop) {
auto it = map.find(folly::Random::rand32() % N);
++readTimes;
}
});
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
stop = true;
writer.join();
for (auto &reader : readers) {
reader.join();
}
auto elapsed = std::chrono::steady_clock::now() - start;
auto ratio = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
fmt::print("OPS: {} read, {} write\n", readTimes.load() * 1000 / ratio, writeTimes.load() * 1000 / ratio);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,29 @@
#include <chrono>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/CurrentExecutor.h>
#include <folly/experimental/coro/FutureUtil.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/Promise.h>
#include <folly/io/async/EventBase.h>
#include <folly/portability/GTest.h>
#include <thread>
#include <vector>
TEST(TestEventBase, Normal) {
folly::EventBase evb;
bool finished = false;
folly::coro::co_invoke([&]() -> folly::coro::Task<void> {
auto executor = co_await folly::coro::co_current_executor;
EXPECT_EQ(executor, &evb);
finished = true;
evb.terminateLoopSoon();
co_return;
})
.scheduleOn(&evb)
.start();
evb.loopForever();
ASSERT_TRUE(finished);
}

View File

@@ -0,0 +1,341 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/IPAddressV6.h>
#include <folly/SocketAddress.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/io/async/test/AsyncSocketTest.h>
#include <folly/io/async/test/MockAsyncTransport.h>
#include <folly/io/async/test/ScopedBoundPort.h>
#include <folly/io/coro/ServerSocket.h>
#include <folly/io/coro/Transport.h>
#include <folly/portability/GTest.h>
static_assert(FOLLY_HAS_COROUTINES, "");
#if FOLLY_HAS_COROUTINES
using namespace std::chrono_literals;
using namespace folly;
using namespace folly::coro;
class TransportTest : public testing::Test {
public:
template <typename F>
void run(F f) {
blockingWait(co_invoke(std::move(f)), &evb);
}
folly::coro::Task<> requestCancellation() {
cancelSource.requestCancellation();
co_return;
}
EventBase evb;
CancellationSource cancelSource;
};
class ServerTransportTest : public TransportTest {
public:
folly::coro::Task<Transport> connect() {
co_return co_await Transport::newConnectedSocket(&evb, srv.getAddress(), 0ms);
}
TestServer srv;
};
TEST_F(TransportTest, ConnectFailure) {
run([&]() -> Task<> {
// note: currently, docker CI runner doesn't support IPv6
ScopedBoundPort ph(IPAddressV4("0.0.0.0"));
auto serverAddr = ph.getAddress();
EXPECT_THROW(co_await Transport::newConnectedSocket(&evb, serverAddr, 0ms), AsyncSocketException);
});
}
TEST_F(ServerTransportTest, ConnectSuccess) {
run([&]() -> Task<> {
auto cs = co_await connect();
EXPECT_EQ(srv.getAddress(), cs.getPeerAddress());
});
}
TEST_F(ServerTransportTest, ConnectCancelled) {
run([&]() -> Task<> {
co_await folly::coro::collectAll(
// token would be cancelled while waiting on connect
[&]() -> Task<> {
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(), connect()), OperationCancelled);
}(),
requestCancellation());
// token was cancelled before read was called
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(),
Transport::newConnectedSocket(&evb, srv.getAddress(), 0ms)),
OperationCancelled);
});
}
TEST_F(ServerTransportTest, SimpleRead) {
run([&]() -> Task<> {
constexpr auto kBufSize = 65536;
auto cs = co_await connect();
// produces blocking socket
auto ss = srv.accept(-1);
std::array<uint8_t, kBufSize> sndBuf;
std::memset(sndBuf.data(), 'a', sndBuf.size());
ss->write(sndBuf.data(), sndBuf.size());
// read using coroutines
std::array<uint8_t, kBufSize> rcvBuf;
auto reader = [&rcvBuf, &cs]() -> Task<Unit> {
int totalBytes{0};
while (totalBytes < kBufSize) {
auto bytesRead =
co_await cs.read(MutableByteRange(rcvBuf.data() + totalBytes, (rcvBuf.data() + rcvBuf.size() - totalBytes)),
0ms);
totalBytes += bytesRead;
}
co_return unit;
};
co_await reader();
EXPECT_EQ(0, memcmp(sndBuf.data(), rcvBuf.data(), rcvBuf.size()));
});
}
TEST_F(ServerTransportTest, SimpleIOBufRead) {
run([&]() -> Task<> {
// Exactly fills a buffer mid-loop and triggers deferredReadEOF handling
constexpr auto kBufSize = 55 * 1184;
auto cs = co_await connect();
// produces blocking socket
auto ss = srv.accept(-1);
std::array<uint8_t, kBufSize> sndBuf;
std::memset(sndBuf.data(), 'a', sndBuf.size());
ss->write(sndBuf.data(), sndBuf.size());
ss->close();
// read using coroutines
IOBufQueue rcvBuf(IOBufQueue::cacheChainLength());
int totalBytes{0};
while (totalBytes < kBufSize) {
auto bytesRead = co_await cs.read(rcvBuf, 1000, 1000, 0ms);
totalBytes += bytesRead;
}
auto bytesRead = co_await cs.read(rcvBuf, 1000, 1000, 50ms);
EXPECT_EQ(bytesRead, 0); // closed
auto data = rcvBuf.move();
data->coalesce();
EXPECT_EQ(0, memcmp(sndBuf.data(), data->data(), data->length()));
});
}
TEST_F(ServerTransportTest, ReadCancelled) {
run([&]() -> Task<> {
auto cs = co_await connect();
auto reader = [&cs]() -> Task<Unit> {
std::array<uint8_t, 1024> rcvBuf;
EXPECT_THROW(co_await cs.read(MutableByteRange(rcvBuf.data(), (rcvBuf.data() + rcvBuf.size())), 0ms),
OperationCancelled);
co_return unit;
};
co_await co_withCancellation(cancelSource.getToken(), folly::coro::collectAll(requestCancellation(), reader()));
// token was cancelled before read was called
co_await co_withCancellation(cancelSource.getToken(), reader());
});
}
TEST_F(ServerTransportTest, ReadTimeout) {
run([&]() -> Task<> {
auto cs = co_await connect();
std::array<uint8_t, 1024> rcvBuf;
EXPECT_THROW(co_await cs.read(MutableByteRange(rcvBuf.data(), (rcvBuf.data() + rcvBuf.size())), 50ms),
AsyncSocketException);
});
}
TEST_F(ServerTransportTest, ReadError) {
run([&]() -> Task<> {
auto cs = co_await connect();
// produces blocking socket
auto ss = srv.accept(-1);
ss->closeWithReset();
std::array<uint8_t, 1024> rcvBuf;
EXPECT_THROW(co_await cs.read(MutableByteRange(rcvBuf.data(), (rcvBuf.data() + rcvBuf.size())), 50ms),
AsyncSocketException);
});
}
TEST_F(ServerTransportTest, SimpleWrite) {
run([&]() -> Task<> {
auto cs = co_await connect();
// produces blocking socket
auto ss = srv.accept(-1);
constexpr auto kBufSize = 65536;
std::array<uint8_t, kBufSize> sndBuf;
std::memset(sndBuf.data(), 'a', sndBuf.size());
// write use co-routine
co_await cs.write(ByteRange(sndBuf.data(), sndBuf.data() + sndBuf.size()));
// read on server side
std::array<uint8_t, kBufSize> rcvBuf;
ss->readAll(rcvBuf.data(), rcvBuf.size());
EXPECT_EQ(0, memcmp(sndBuf.data(), rcvBuf.data(), rcvBuf.size()));
});
}
TEST_F(ServerTransportTest, SimpleWritev) {
run([&]() -> Task<> {
auto cs = co_await connect();
// produces blocking socket
auto ss = srv.accept(-1);
IOBufQueue sndBuf;
constexpr auto kBufSize = 65536;
std::array<uint8_t, kBufSize> bufA;
std::memset(bufA.data(), 'a', bufA.size());
std::array<uint8_t, kBufSize> bufB;
std::memset(bufB.data(), 'b', bufB.size());
sndBuf.append(bufA.data(), bufA.size());
sndBuf.append(bufB.data(), bufB.size());
// write use co-routine
co_await cs.write(sndBuf);
// read on server side
std::array<uint8_t, kBufSize> rcvBufA;
ss->readAll(rcvBufA.data(), rcvBufA.size());
EXPECT_EQ(0, memcmp(bufA.data(), rcvBufA.data(), rcvBufA.size()));
std::array<uint8_t, kBufSize> rcvBufB;
ss->readAll(rcvBufB.data(), rcvBufB.size());
EXPECT_EQ(0, memcmp(bufB.data(), rcvBufB.data(), rcvBufB.size()));
});
}
TEST_F(ServerTransportTest, WriteCancelled) {
run([&]() -> Task<> {
auto cs = co_await connect();
// reduce the send buffer size so the write wouldn't complete immediately
auto asyncSocket = dynamic_cast<folly::AsyncSocket *>(cs.getTransport());
CHECK(asyncSocket);
EXPECT_EQ(asyncSocket->setSendBufSize(4096), 0);
// produces blocking socket
auto ss = srv.accept(-1);
constexpr auto kBufSize = 65536;
std::array<uint8_t, kBufSize> sndBuf;
std::memset(sndBuf.data(), 'a', sndBuf.size());
// write use co-routine
auto writer = [&]() -> Task<> {
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(),
cs.write(ByteRange(sndBuf.data(), sndBuf.data() + sndBuf.size()))),
OperationCancelled);
};
co_await folly::coro::collectAll(requestCancellation(), writer());
co_await co_withCancellation(cancelSource.getToken(), writer());
});
}
TEST_F(TransportTest, SimpleAccept) {
run([&]() -> Task<> {
ScopedBoundPort ph(IPAddressV4("0.0.0.0"));
ServerSocket css(AsyncServerSocket::newSocket(&evb), ph.getAddress(), 16);
auto serverAddr = css.getAsyncServerSocket()->getAddress();
co_await folly::coro::collectAll(css.accept(), Transport::newConnectedSocket(&evb, serverAddr, 0ms));
});
}
TEST_F(TransportTest, AcceptCancelled) {
run([&]() -> Task<> {
co_await folly::coro::collectAll(requestCancellation(), [&]() -> Task<> {
ServerSocket css(AsyncServerSocket::newSocket(&evb), std::nullopt, 16);
EXPECT_THROW(co_await co_withCancellation(cancelSource.getToken(), css.accept()), OperationCancelled);
}());
});
}
TEST_F(TransportTest, AsyncClientAndServer) {
run([&]() -> Task<> {
constexpr int kSize = 128;
ScopedBoundPort ph(IPAddressV4("0.0.0.0"));
ServerSocket css(AsyncServerSocket::newSocket(&evb), ph.getAddress(), 16);
auto serverAddr = css.getAsyncServerSocket()->getAddress();
auto cs = co_await Transport::newConnectedSocket(&evb, serverAddr, 0ms);
co_await folly::coro::collectAll(
[&css]() -> Task<> {
auto sock = co_await css.accept();
std::array<uint8_t, kSize> buf;
memset(buf.data(), 'a', kSize);
co_await sock->write(ByteRange(buf.begin(), buf.end()));
css.close();
}(),
[&cs]() -> Task<> {
std::array<uint8_t, kSize> buf;
// For fun, shutdown the write half -- we don't need it
cs.shutdownWrite();
auto len = co_await cs.read(MutableByteRange(buf.begin(), buf.end()), 0ms);
cs.close();
EXPECT_TRUE(len == buf.size());
}());
});
}
class MockTransportTest : public TransportTest {
public:
folly::coro::Task<Transport> connect() {
mockTransport = new testing::NiceMock<test::MockAsyncTransport>();
folly::AsyncTransport::UniquePtr transport(mockTransport);
co_return Transport(&evb, std::move(transport));
}
test::MockAsyncTransport *mockTransport;
};
TEST_F(MockTransportTest, readSuccessCanceled) {
run([&]() -> Task<> {
auto cs = co_await connect();
constexpr auto kBufSize = 65536;
std::array<uint8_t, kBufSize> rcvBuf;
EXPECT_CALL(*mockTransport, setReadCB(testing::_)).WillOnce(testing::Invoke([](AsyncReader::ReadCallback *rcb) {
rcb->readEOF();
}));
EXPECT_CALL(*mockTransport, setReadCB(nullptr)).Times(2);
folly::CancellationSource cancellationSource;
auto readFut = co_withCancellation(cancellationSource.getToken(),
cs.read(MutableByteRange(rcvBuf.data(), rcvBuf.data() + rcvBuf.size()), 100ms))
.scheduleOn(&evb)
.start();
// Let the read coro start and get the EOF
co_await co_reschedule_on_current_executor;
// cancel
cancellationSource.requestCancellation();
// read succeeds with nRead == 0
auto nRead = co_await std::move(readFut);
EXPECT_EQ(nRead, 0);
});
}
#endif // FOLLY_HAS_COROUTINES

View File

@@ -0,0 +1,48 @@
#include <chrono>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/FutureUtil.h>
#include <folly/experimental/coro/Promise.h>
#include <folly/portability/GTest.h>
#include <thread>
#include <vector>
using namespace folly::coro;
TEST(CoroWaitCallbackTest, WaitCallback) {
bool same_thread = false;
auto task = [&same_thread]() -> folly::coro::Task<void> {
auto tid_before = std::this_thread::get_id();
auto [promise, future] = makePromiseContract<void>();
std::thread notify_at_another_thread([promise = std::move(promise)]() mutable {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
promise.setValue();
});
co_await toTask(std::move(future));
auto tid_after = std::this_thread::get_id();
same_thread = tid_before == tid_after;
notify_at_another_thread.join();
};
blockingWait(task());
ASSERT_TRUE(same_thread);
}
TEST(CoroWaitCallbackTest, MultiTasks) {
int orders = 0;
auto create_task = [&orders]() -> folly::coro::Task<void> {
auto [promise, future] = makePromiseContract<void>();
std::thread notify_at_another_thread([promise = std::move(promise)]() mutable {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
promise.setValue();
});
orders = orders * 10 + 1;
co_await toTask(std::move(future));
orders = orders * 10 + 2;
notify_at_another_thread.join();
};
blockingWait(collectAll(create_task(), create_task()));
ASSERT_EQ(orders, 1122);
}

View File

@@ -0,0 +1,29 @@
#include <gtest/gtest.h>
#include <string>
#include "common/kv/KeyPrefix.h"
#include "fdb/FDB.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::tests {
namespace {
TEST(KeyPrefixTest, test) {
ASSERT_EQ(static_cast<uint32_t>(kv::KeyPrefix::Inode), kv::makePrefixValue("INOD"));
ASSERT_EQ(kv::toStr(kv::KeyPrefix::Inode), "INOD");
ASSERT_EQ(kv::toStr(static_cast<kv::KeyPrefix>(-1)), "UNKW");
}
TEST(PrefixListEndKey, test) {
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a"), std::string("b"));
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a1"), std::string("a2"));
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a\xff"), std::string("b"));
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a\xff\xff"), std::string("b"));
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("a\xff\xff\xff"), std::string("b"));
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("\xff"
"a\xff\xff"),
std::string("\xff"
"b"));
ASSERT_EQ(kv::TransactionHelper::prefixListEndKey("\xff\xff"), std::string(""));
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,27 @@
#pragma once
#include <gtest/gtest.h>
#include <memory>
#include <thread>
#include "fdb/FDB.h"
#include "fdb/FDBContext.h"
#include "gtest/gtest.h"
#include "tests/fdb/SetupFDB.h"
namespace hf3fs::kv {
class FDBTestBase : public testing::SetupFDB {
protected:
void SetUp() override {
auto cluster = std::getenv("FDB_UNITTEST_CLUSTER");
if (!cluster) {
GTEST_SKIP_("FDB_UNITTEST_CLUSTER not set skip test");
}
db_ = fdb::DB(cluster, false);
}
protected:
fdb::DB db_;
};
} // namespace hf3fs::kv

View File

@@ -0,0 +1,220 @@
#include <cstdint>
#include <fmt/core.h>
#include <folly/Random.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/logging/xlog.h>
#include <gtest/gtest.h>
#include <optional>
#include <string_view>
#include <unistd.h>
#include <utility>
#include <vector>
#include "FDBTestBase.h"
namespace hf3fs::kv {
class TestFDB : public FDBTestBase {};
TEST_F(TestFDB, WaitFuture) {
ASSERT_EQ(db_.error(), 0);
folly::coro::blockingWait([this]() -> folly::coro::Task<void> {
fdb::Transaction tr(db_);
auto sizeResult = co_await tr.getApproximateSize();
EXPECT_TRUE(sizeResult.error() == 0);
auto result = co_await tr.get("foo", true);
EXPECT_TRUE(result.error() == 0);
EXPECT_FALSE(result.value().has_value());
}());
}
TEST_F(TestFDB, GetRange) {
ASSERT_EQ(db_.error(), 0);
::FDBTransaction *tr = nullptr;
std::string startKey = "unittest.get_range";
std::string endKey = "unittest.get_rangez";
// 1. set
{
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
for (int i = 0; i < 100; ++i) {
auto k = startKey + fmt::format("{:02d}", i);
auto v = fmt::format("unittest.value{:02d}", i);
fdb_transaction_set(tr, (const uint8_t *)k.data(), k.length(), (const uint8_t *)v.data(), v.length());
}
auto f = fdb_transaction_commit(tr);
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
ASSERT_EQ(fdb_future_get_error(f), 0);
fdb_future_destroy(f);
fdb_transaction_destroy(tr);
}
// 2. get range
{
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
auto f =
fdb_transaction_get_range(tr,
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL((const uint8_t *)startKey.data(), startKey.size()),
FDB_KEYSEL_FIRST_GREATER_THAN((const uint8_t *)endKey.data(), endKey.size()),
10,
0,
FDB_STREAMING_MODE_SERIAL,
0,
false,
false);
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
ASSERT_EQ(fdb_future_get_error(f), 0);
const FDBKeyValue *kv = nullptr;
int count = 0;
fdb_bool_t more = false;
ASSERT_EQ(fdb_future_get_keyvalue_array(f, &kv, &count, &more), 0);
ASSERT_TRUE(more);
ASSERT_EQ(count, 10);
fdb_future_destroy(f);
fdb_transaction_destroy(tr);
}
{
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
auto f =
fdb_transaction_get_range(tr,
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL((const uint8_t *)startKey.data(), startKey.size()),
FDB_KEYSEL_FIRST_GREATER_THAN((const uint8_t *)endKey.data(), endKey.size()),
0,
0,
FDB_STREAMING_MODE_SERIAL,
0,
false,
false);
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
ASSERT_EQ(fdb_future_get_error(f), 0);
const FDBKeyValue *kv = nullptr;
int count = 0;
fdb_bool_t more = false;
ASSERT_EQ(fdb_future_get_keyvalue_array(f, &kv, &count, &more), 0);
ASSERT_FALSE(more);
ASSERT_EQ(count, 100);
fdb_future_destroy(f);
fdb_transaction_destroy(tr);
}
{
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
auto f =
fdb_transaction_get_range(tr,
FDB_KEYSEL_FIRST_GREATER_OR_EQUAL((const uint8_t *)startKey.data(), startKey.size()),
FDB_KEYSEL_FIRST_GREATER_THAN((const uint8_t *)endKey.data(), endKey.size()),
-1,
0,
FDB_STREAMING_MODE_SERIAL,
0,
false,
false);
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
ASSERT_EQ(fdb_future_get_error(f), 0);
const FDBKeyValue *kv = nullptr;
int count = 0;
fdb_bool_t more = false;
ASSERT_EQ(fdb_future_get_keyvalue_array(f, &kv, &count, &more), 0);
ASSERT_FALSE(more);
ASSERT_EQ(count, 100);
fdb_future_destroy(f);
fdb_transaction_destroy(tr);
}
// 3. clear
{
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
for (int i = 0; i < 100; ++i) {
auto k = startKey + fmt::format("{:02d}", i);
fdb_transaction_clear(tr, (const uint8_t *)k.data(), k.length());
}
auto f = fdb_transaction_commit(tr);
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
ASSERT_EQ(fdb_future_get_error(f), 0);
fdb_future_destroy(f);
fdb_transaction_destroy(tr);
}
}
TEST_F(TestFDB, Version) {
ASSERT_EQ(db_.error(), 0);
for (auto delay : {0, 10, 100, 200, 500, 1000}) {
::FDBTransaction *tr1 = nullptr;
::FDBTransaction *tr2 = nullptr;
ASSERT_EQ(fdb_database_create_transaction(db_, &tr1), 0);
ASSERT_EQ(fdb_database_create_transaction(db_, &tr2), 0);
std::string_view key = "random-key";
fdb_transaction_clear(tr1, (uint8_t *)key.data(), key.size());
fdb_transaction_clear(tr2, (uint8_t *)key.data(), key.size());
auto f1 = fdb_transaction_commit(tr1);
if (delay) usleep(delay);
auto f2 = fdb_transaction_commit(tr2);
ASSERT_EQ(fdb_future_block_until_ready(f1), 0);
ASSERT_EQ(fdb_future_get_error(f1), 0);
ASSERT_EQ(fdb_future_block_until_ready(f2), 0);
ASSERT_EQ(fdb_future_get_error(f2), 0);
int64_t v1, v2;
ASSERT_EQ(fdb_transaction_get_committed_version(tr1, &v1), 0);
ASSERT_EQ(fdb_transaction_get_committed_version(tr2, &v2), 0);
fmt::print("delay {}us, v1 {}, v2 {}, v1 == v2 {}\n", delay, v1, v2, v1 == v2);
fdb_future_destroy(f1);
fdb_transaction_destroy(tr1);
fdb_future_destroy(f2);
fdb_transaction_destroy(tr2);
}
::FDBTransaction *tr = nullptr;
std::string_view key = "test-version";
std::vector<std::pair<int64_t, std::optional<std::string_view>>> vec;
for (auto value :
std::vector<std::optional<std::string_view>>{std::nullopt, std::optional("value1"), std::optional("value2")}) {
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
if (value) {
fdb_transaction_set(tr, (uint8_t *)key.data(), key.size(), (uint8_t *)value->data(), value->size());
} else {
fdb_transaction_clear(tr, (uint8_t *)key.data(), key.size());
}
int64_t version;
auto f = fdb_transaction_commit(tr);
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
ASSERT_EQ(fdb_future_get_error(f), 0);
fdb_future_destroy(f);
ASSERT_EQ(fdb_transaction_get_committed_version(tr, &version), 0);
fdb_transaction_destroy(tr);
vec.push_back({version, value});
}
for (auto [version, expected] : vec) {
ASSERT_EQ(fdb_database_create_transaction(db_, &tr), 0);
fdb_transaction_set_read_version(tr, version);
auto f = fdb_transaction_get(tr, (uint8_t *)key.data(), key.size(), true);
ASSERT_EQ(fdb_future_block_until_ready(f), 0);
ASSERT_EQ(fdb_future_get_error(f), 0);
const uint8_t *value = nullptr;
int length = 0;
fdb_bool_t present = false;
ASSERT_EQ(fdb_future_get_value(f, &present, &value, &length), 0);
if (!expected) {
ASSERT_FALSE(present);
} else {
ASSERT_TRUE(present);
std::string str((const char *)value, length);
ASSERT_EQ(str, *expected);
}
fdb_future_destroy(f);
fdb_transaction_destroy(tr);
}
}
} // namespace hf3fs::kv

View File

@@ -0,0 +1,17 @@
#include <gtest/gtest.h>
#include "FDBTestBase.h"
#include "fdb/FDBKVEngine.h"
namespace hf3fs::kv {
class TestFDBKVEngine : public FDBTestBase {};
TEST_F(TestFDBKVEngine, Construct) {
ASSERT_EQ(db_.error(), 0);
FDBKVEngine engine(std::move(db_));
ASSERT_TRUE(engine.createReadonlyTransaction() != nullptr);
ASSERT_TRUE(engine.createReadWriteTransaction() != nullptr);
}
} // namespace hf3fs::kv

View File

@@ -0,0 +1,306 @@
#include <chrono>
#include <cstdint>
#include <folly/CancellationToken.h>
#include <folly/Random.h>
#include <folly/ScopeGuard.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <gtest/gtest.h>
#include <numeric>
#include <ostream>
#include <set>
#include "FDBTestBase.h"
#include "common/utils/Coroutine.h"
#include "common/utils/Duration.h"
#include "common/utils/UtcTime.h"
#include "common/utils/Uuid.h"
#include "fdb/FDBKVEngine.h"
#include "fdb/FDBTransaction.h"
#include "fmt/core.h"
#include "foundationdb/fdb_c_options.g.h"
#include "foundationdb/fdb_c_types.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::kv {
class TestFDBTransaction : public FDBTestBase {
public:
void SetUp() override {
FDBTestBase::SetUp();
engine_ = FDBKVEngine(std::move(db_));
}
Status convertError(fdb_error_t errCode, bool commit) { return FDBTransaction::testFDBError(errCode, commit); }
protected:
constexpr static auto testKey = "unittest.foo";
constexpr static auto testKey2 = "unittest.foo1";
constexpr static auto testKey3 = "unittest.foo2";
constexpr static auto testValue = "unittest.bar";
constexpr static auto conflictKey = "unittest.conflict.";
FDBKVEngine engine_{std::move(db_)};
};
TEST_F(TestFDBTransaction, SetValue) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadWriteTransaction();
auto result = co_await transaction->set(testKey, testValue);
EXPECT_TRUE(result.hasValue());
result = co_await transaction->set(testKey2, testValue);
EXPECT_TRUE(result.hasValue());
result = co_await transaction->set(testKey3, testValue);
EXPECT_TRUE(result.hasValue());
auto commit = co_await transaction->commit();
EXPECT_TRUE(commit.hasValue());
}());
}
TEST_F(TestFDBTransaction, SnapshotGet) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
auto result = co_await transaction->snapshotGet(testKey);
EXPECT_TRUE(result.hasValue());
EXPECT_TRUE(result.value().has_value());
EXPECT_EQ(result.value().value(), testValue);
}());
}
TEST_F(TestFDBTransaction, SnapshotGetSameKey) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
std::set<uint64_t> latency;
for (size_t i = 0; i < 100; i++) {
auto begin = SteadyClock::now();
auto result = co_await transaction->snapshotGet(testKey);
EXPECT_TRUE(result.hasValue());
EXPECT_TRUE(result.value().has_value());
EXPECT_EQ(result.value().value(), testValue);
auto lat = SteadyClock::now() - begin;
latency.insert(lat.count() / 1000);
}
fmt::print("snapshot get same key, max {}us, min {}us, avg {}us\n",
*latency.rbegin(),
*latency.begin(),
std::accumulate(latency.begin(), latency.end(), 0ul) / latency.size());
}());
}
TEST_F(TestFDBTransaction, GetRangeSnapshot) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
IReadOnlyTransaction::KeySelector begin(testKey, true);
IReadOnlyTransaction::KeySelector end(testKey, true);
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
EXPECT_TRUE(result.hasValue());
auto value = std::move(result.value());
EXPECT_EQ(value.kvs.size(), 1);
EXPECT_EQ(value.kvs.front().key, testKey);
EXPECT_FALSE(value.hasMore);
}());
}
TEST_F(TestFDBTransaction, GetRangeSnapshot2) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
IReadOnlyTransaction::KeySelector begin(testKey, false);
IReadOnlyTransaction::KeySelector end(testKey3, false);
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
EXPECT_TRUE(result.hasValue());
auto value = std::move(result.value());
EXPECT_EQ(value.kvs.size(), 1);
EXPECT_EQ(value.kvs.front().key, testKey2);
EXPECT_FALSE(value.hasMore);
}());
}
TEST_F(TestFDBTransaction, GetRangeSnapshot3) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
IReadOnlyTransaction::KeySelector begin(testKey, true);
IReadOnlyTransaction::KeySelector end(testKey3, true);
auto result = co_await transaction->snapshotGetRange(begin, end, 2);
EXPECT_TRUE(result.hasValue());
auto value = std::move(result.value());
EXPECT_EQ(value.kvs.size(), 2);
EXPECT_EQ(value.kvs.front().key, testKey);
EXPECT_TRUE(value.hasMore);
}());
}
TEST_F(TestFDBTransaction, Get) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadWriteTransaction();
auto result = co_await transaction->get(testKey);
EXPECT_TRUE(result.hasValue());
EXPECT_TRUE(result.value().has_value());
EXPECT_EQ(result.value().value(), testValue);
}());
}
TEST_F(TestFDBTransaction, Clear) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadWriteTransaction();
auto result = co_await transaction->clear(testKey);
EXPECT_TRUE(result.hasValue());
result = co_await transaction->clear(testKey2);
EXPECT_TRUE(result.hasValue());
result = co_await transaction->clear(testKey3);
EXPECT_TRUE(result.hasValue());
auto commit = co_await transaction->commit();
EXPECT_TRUE(commit.hasValue());
auto cancel = co_await transaction->cancel();
EXPECT_TRUE(cancel.hasValue());
}());
}
TEST_F(TestFDBTransaction, TransactionConflict) {
constexpr auto M = 8;
constexpr auto N = 100;
enum OP { GET, SNAPSHOT_GET, ADD_READ_CONFLICT };
for (const auto op : {GET, SNAPSHOT_GET, ADD_READ_CONFLICT}) {
std::atomic<size_t> conflictTimes{0};
folly::CPUThreadPoolExecutor executor(M);
auto parallelSet = [&]() -> CoTask<void> {
for (auto i = 0; i < M; ++i) {
auto func = [&]() -> CoTask<void> {
for (auto j = 0; j < N; ++j) {
auto tr = engine_.createReadWriteTransaction();
auto key = conflictKey + std::to_string(j % 100);
switch (op) {
case GET:
co_await tr->get(key);
break;
case SNAPSHOT_GET:
co_await tr->snapshotGet(key);
break;
case ADD_READ_CONFLICT:
default:
co_await tr->addReadConflict(key);
break;
}
co_await tr->set(key, key);
auto result = co_await tr->commit();
if (result.hasError()) {
if (result.error().code() == TransactionCode::kConflict) {
++conflictTimes;
}
}
}
co_return;
};
folly::coro::co_invoke(func).scheduleOn(co_await folly::coro::co_current_executor).start();
}
};
folly::coro::blockingWait(folly::coro::co_invoke(parallelSet).scheduleOn(&executor));
executor.join();
fmt::print("Transaction conflict times: {}\n", conflictTimes.load());
if (op != SNAPSHOT_GET) {
ASSERT_NE(conflictTimes, 0);
} else {
ASSERT_EQ(conflictTimes, 0);
}
}
}
TEST_F(TestFDBTransaction, CancellationSafety) {
folly::CPUThreadPoolExecutor exec(1);
folly::CancellationSource source;
auto transaction = engine_.createReadWriteTransaction();
auto future =
folly::coro::co_withCancellation(source.getToken(), transaction->get("not found key")).scheduleOn(&exec).start();
source.requestCancellation();
ASSERT_THROW(future.wait().value(), folly::OperationCancelled);
}
TEST_F(TestFDBTransaction, TestErrorcode) {
for (int errCode = 1; errCode < 10000; errCode++) {
if (errCode == 1039) continue;
auto status = convertError(errCode, false);
ASSERT_EQ(TransactionHelper::isRetryable(status, false),
fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE_NOT_COMMITTED, errCode))
<< status.describe();
ASSERT_EQ(TransactionHelper::isRetryable(status, true), fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE, errCode))
<< status.describe();
}
for (int errCode = 1; errCode < 10000; errCode++) {
auto status = convertError(errCode, true);
if (errCode == 1039) continue;
ASSERT_EQ(TransactionHelper::isRetryable(status, false),
fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE_NOT_COMMITTED, errCode))
<< status.describe();
ASSERT_EQ(TransactionHelper::isRetryable(status, true), fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE, errCode))
<< status.describe();
}
}
TEST_F(TestFDBTransaction, VersionstampedKey) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto prefix = fmt::format("test.{}", Uuid::random());
auto key = fmt::format("{}0000000000", prefix);
for (size_t i = 0; i < 100; i++) {
auto tr = engine_.createReadWriteTransaction();
CO_ASSERT_OK(co_await tr->setVersionstampedKey(key, prefix.size(), folly::to<std::string>(i)));
CO_ASSERT_OK(co_await tr->commit());
}
auto tr = engine_.createReadonlyTransaction();
auto result = co_await tr->getRange({prefix, true}, {TransactionHelper::prefixListEndKey(prefix), false}, 120);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->kvs.size(), 100);
for (size_t i = 0; i < 100; i++) {
auto [key, value] = result->kvs.at(i).pair();
CO_ASSERT_EQ(key.substr(0, prefix.size()), prefix);
CO_ASSERT_EQ(key.size(), prefix.size() + 10);
CO_ASSERT_EQ(value, folly::to<std::string>(i));
}
}());
}
TEST_F(TestFDBTransaction, VersionstampedValue) {
std::string prev;
folly::coro::blockingWait([&]() -> CoTask<void> {
auto key = fmt::format("test.{}-", Uuid::random());
std::string buffer(1024, 'a');
for (size_t i = 0; i < 100; i++) {
auto offset = folly::Random::rand32(1024 - 10);
{
auto tr = engine_.createReadWriteTransaction();
CO_ASSERT_OK(co_await tr->setVersionstampedValue(key, buffer, offset));
CO_ASSERT_OK(co_await tr->commit());
}
{
auto tr = engine_.createReadonlyTransaction();
auto value = co_await tr->get(key);
CO_ASSERT_OK(value);
CO_ASSERT_TRUE(value->has_value());
auto str = **value;
CO_ASSERT_EQ(str.substr(0, offset), buffer.substr(0, offset));
CO_ASSERT_GE(str.substr(offset, 10), prev);
CO_ASSERT_EQ(str.substr(offset + 10), buffer.substr(offset + 10));
prev = str.substr(offset, 10);
}
}
}());
}
TEST_F(TestFDBTransaction, Readonly) {
folly::coro::blockingWait([&]() -> CoTask<void> {
engine_.setReadonly(true);
SCOPE_EXIT { engine_.setReadonly(false); };
auto transaction = engine_.createReadWriteTransaction();
auto commit = co_await transaction->commit();
EXPECT_TRUE(commit.hasError());
}());
}
} // namespace hf3fs::kv

View File

@@ -0,0 +1,295 @@
#include <double-conversion/utils.h>
#include <folly/Conv.h>
#include <folly/Random.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <gtest/gtest.h>
#include <string>
#include <string_view>
#include "common/kv/IKVEngine.h"
#include "common/kv/ITransaction.h"
#include "common/kv/mem/MemKV.h"
#include "common/kv/mem/MemKVEngine.h"
#include "fmt/core.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::kv {
class TestMemTransaction : public ::testing::Test {
protected:
constexpr static auto testKey = "unittest.foo";
constexpr static auto testKey2 = "unittest.foo1";
constexpr static auto testKey3 = "unittest.foo2";
constexpr static auto testValue = "unittest.bar";
CoTryTask<void> setupTestKeys() {
auto transaction = engine_.createReadWriteTransaction();
auto result = co_await transaction->set(testKey, testValue);
CO_RETURN_ON_ERROR(result);
result = co_await transaction->set(testKey2, testValue);
CO_RETURN_ON_ERROR(result);
result = co_await transaction->set(testKey3, testValue);
CO_RETURN_ON_ERROR(result);
auto commit = co_await transaction->commit();
CO_RETURN_ON_ERROR(commit);
co_return Void();
}
void blockingSetupTestKeys() {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto result = co_await setupTestKeys();
EXPECT_TRUE(result.hasValue());
}());
}
MemKVEngine engine_;
};
TEST_F(TestMemTransaction, SetValue) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto oldTransaction = engine_.createReadWriteTransaction();
auto result = co_await setupTestKeys();
EXPECT_TRUE(result.hasValue());
auto readOldResult = co_await oldTransaction->get(testKey);
EXPECT_TRUE(readOldResult.hasValue());
EXPECT_FALSE(readOldResult.value().has_value());
auto newTransaction = engine_.createReadWriteTransaction();
auto readNewResult = co_await newTransaction->get(testKey);
EXPECT_TRUE(readNewResult.hasValue());
EXPECT_TRUE(readNewResult.value().has_value());
}());
}
TEST_F(TestMemTransaction, SnapshotGet) {
blockingSetupTestKeys();
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
auto result = co_await transaction->snapshotGet(testKey);
EXPECT_TRUE(result.hasValue());
EXPECT_TRUE(result.value().has_value());
EXPECT_EQ(result.value().value(), testValue);
}());
}
TEST_F(TestMemTransaction, GetRangeSnapshot) {
blockingSetupTestKeys();
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
IReadOnlyTransaction::KeySelector begin(testKey, true);
IReadOnlyTransaction::KeySelector end(testKey, true);
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
EXPECT_TRUE(result.hasValue());
auto value = std::move(result.value());
EXPECT_EQ(value.kvs.size(), 1);
EXPECT_EQ(value.kvs.front().key, testKey);
EXPECT_FALSE(value.hasMore);
}());
}
TEST_F(TestMemTransaction, GetRangeSnapshot2) {
blockingSetupTestKeys();
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
IReadOnlyTransaction::KeySelector begin(testKey, false);
IReadOnlyTransaction::KeySelector end(testKey3, false);
auto result = co_await transaction->snapshotGetRange(begin, end, 100);
EXPECT_TRUE(result.hasValue());
auto value = std::move(result.value());
EXPECT_EQ(value.kvs.size(), 1);
EXPECT_EQ(value.kvs.front().key, testKey2);
EXPECT_FALSE(value.hasMore);
}());
}
TEST_F(TestMemTransaction, GetRangeSnapshot3) {
blockingSetupTestKeys();
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadonlyTransaction();
IReadOnlyTransaction::KeySelector begin(testKey, true);
IReadOnlyTransaction::KeySelector end(testKey3, true);
auto result = co_await transaction->snapshotGetRange(begin, end, 2);
EXPECT_TRUE(result.hasValue());
auto value = std::move(result.value());
EXPECT_EQ(value.kvs.size(), 2);
EXPECT_EQ(value.kvs.front().key, testKey);
EXPECT_TRUE(value.hasMore);
}());
}
TEST_F(TestMemTransaction, Get) {
blockingSetupTestKeys();
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadWriteTransaction();
auto result = co_await transaction->get(testKey);
EXPECT_TRUE(result.hasValue());
EXPECT_TRUE(result.value().has_value());
EXPECT_EQ(result.value().value(), testValue);
}());
}
TEST_F(TestMemTransaction, Clear) {
blockingSetupTestKeys();
folly::coro::blockingWait([&]() -> CoTask<void> {
auto transaction = engine_.createReadWriteTransaction();
auto result = co_await transaction->clear(testKey);
EXPECT_TRUE(result.hasValue());
result = co_await transaction->clear(testKey2);
EXPECT_TRUE(result.hasValue());
result = co_await transaction->clear(testKey3);
EXPECT_TRUE(result.hasValue());
auto commit = co_await transaction->commit();
EXPECT_TRUE(commit.hasValue());
auto cancel = co_await transaction->cancel();
EXPECT_TRUE(cancel.hasValue());
}());
}
TEST_F(TestMemTransaction, TransactionNoConflict) {
constexpr auto M = 8;
constexpr auto N = 10000;
blockingSetupTestKeys();
std::atomic<size_t> conflictTimes{0};
folly::CPUThreadPoolExecutor executor(M);
auto parallelSet = [&]() -> CoTask<void> {
for (auto i = 0; i < M; ++i) {
auto func = [&]() -> CoTask<void> {
for (auto j = 0; j < N; ++j) {
auto tr = engine_.createReadWriteTransaction();
auto key = std::to_string(j % (N / 100));
co_await tr->snapshotGet(key);
co_await tr->set(key, key);
auto result = co_await tr->commit();
if (result.hasError()) {
if (result.error().code() == TransactionCode::kConflict) {
++conflictTimes;
}
}
}
co_return;
};
folly::coro::co_invoke(func).scheduleOn(co_await folly::coro::co_current_executor).start();
}
};
folly::coro::blockingWait(folly::coro::co_invoke(parallelSet).scheduleOn(&executor));
executor.join();
ASSERT_EQ(conflictTimes, 0);
}
TEST_F(TestMemTransaction, TransactionConflict) {
constexpr auto M = 8;
constexpr auto N = 10000;
enum OP { GET, SNAPSHOT_GET, ADD_READ_CONFLICT };
blockingSetupTestKeys();
for (const auto op : {GET, SNAPSHOT_GET, ADD_READ_CONFLICT}) {
std::atomic<size_t> conflictTimes{0};
folly::CPUThreadPoolExecutor executor(M);
auto parallelSet = [&]() -> CoTask<void> {
for (auto i = 0; i < M; ++i) {
auto func = [&]() -> CoTask<void> {
for (auto j = 0; j < N; ++j) {
auto tr = engine_.createReadWriteTransaction();
auto key = std::to_string(j % (N / 100));
switch (op) {
case GET:
co_await tr->get(key);
break;
case SNAPSHOT_GET:
co_await tr->snapshotGet(key);
break;
case ADD_READ_CONFLICT:
default:
co_await tr->addReadConflict(key);
break;
}
co_await tr->set(key, key);
auto result = co_await tr->commit();
if (result.hasError()) {
if (result.error().code() == TransactionCode::kConflict) {
++conflictTimes;
}
}
}
co_return;
};
folly::coro::co_invoke(func).scheduleOn(co_await folly::coro::co_current_executor).start();
}
};
folly::coro::blockingWait(folly::coro::co_invoke(parallelSet).scheduleOn(&executor));
executor.join();
fmt::print("Transaction conflict times: {}\n", conflictTimes.load());
if (op != SNAPSHOT_GET) {
ASSERT_NE(conflictTimes, 0);
} else {
ASSERT_EQ(conflictTimes, 0);
}
}
}
TEST_F(TestMemTransaction, VersionstampedKey) {
folly::coro::blockingWait([&]() -> CoTask<void> {
std::string_view prefix = "key";
auto key = fmt::format("{}0000000000", prefix);
for (size_t i = 0; i < 100; i++) {
auto tr = engine_.createReadWriteTransaction();
CO_ASSERT_OK(co_await tr->setVersionstampedKey(key, prefix.size(), folly::to<std::string>(i)));
CO_ASSERT_OK(co_await tr->commit());
}
CO_ASSERT_EQ(prefix, "key");
auto tr = engine_.createReadonlyTransaction();
auto result = co_await tr->getRange({prefix, true}, {TransactionHelper::prefixListEndKey(prefix), false}, 120);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->kvs.size(), 100);
for (size_t i = 0; i < 100; i++) {
auto [key, value] = result->kvs.at(i).pair();
CO_ASSERT_EQ(key.substr(0, prefix.size()), prefix);
CO_ASSERT_EQ(key.size(), prefix.size() + 10);
CO_ASSERT_EQ(value, folly::to<std::string>(i));
}
}());
}
TEST_F(TestMemTransaction, VersionstampedValue) {
folly::coro::blockingWait([&]() -> CoTask<void> {
std::string key("key");
std::string buffer(1024, 'a');
std::string prev;
for (size_t i = 0; i < 100; i++) {
auto offset = folly::Random::rand32(1024 - 10);
{
auto tr = engine_.createReadWriteTransaction();
CO_ASSERT_OK(co_await tr->setVersionstampedValue(key, buffer, offset));
CO_ASSERT_OK(co_await tr->commit());
}
{
auto tr = engine_.createReadonlyTransaction();
auto value = co_await tr->get(key);
CO_ASSERT_OK(value);
CO_ASSERT_TRUE(value->has_value());
auto str = **value;
CO_ASSERT_EQ(str.substr(0, offset), buffer.substr(0, offset));
CO_ASSERT_GE(str.substr(offset, 10), prev);
CO_ASSERT_EQ(str.substr(offset + 10), buffer.substr(offset + 10));
prev = str.substr(offset, 10);
}
}
}());
}
} // namespace hf3fs::kv

View File

@@ -0,0 +1,215 @@
#include <folly/json.h>
#include <folly/logging/LogConfig.h>
#include <folly/logging/LogConfigParser.h>
#include <folly/logging/LogLevel.h>
#include <gtest/gtest.h>
#include "common/logging/LogConfig.h"
namespace hf3fs::test {
namespace {
using namespace ::hf3fs::logging;
void checkConfig(std::string config, std::string expected) {
auto realObj = folly::parseJson(config);
auto expectedObj = folly::parseJson(expected);
ASSERT_EQ(realObj["categories"], expectedObj["categories"]);
ASSERT_EQ(realObj["handlers"], expectedObj["handlers"]);
ASSERT_NO_THROW(folly::parseLogConfig(config));
}
TEST(TestLogConfig, testDefault) {
LogConfig config;
checkConfig(generateLogConfig(config, "x"), R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
},
"handlers": {
"normal": {
"options": { "path": "x.log", "async": "true", "rotate": "true", "max_files": "100", "max_file_size": "10485760", "rotate_on_open": "false" },
"type": "file"
},
"err": {
"options": { "level": "ERR", "path": "x.err.log", "async": "false", "rotate": "true", "max_files": "100", "max_file_size": "10485760", "rotate_on_open": "false" },
"type": "file"
},
"fatal": {
"options": { "level": "FATAL", "stream": "stderr"},
"type": "stream"
}
}
})JSON");
}
TEST(TestLogConfig, testFileWriter) {
LogHandlerConfig handlerConfig;
handlerConfig.set_name("normal");
handlerConfig.set_writer_type(LogHandlerConfig::WriterType::FILE);
handlerConfig.set_file_path("a.log");
handlerConfig.set_async(false);
handlerConfig.set_rotate(false);
LogConfig config;
// this case does not care about other handlers
config.set_handlers({handlerConfig});
String s = generateLogConfig(config, "x");
checkConfig(s, R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
},
"handlers": {
"normal": {
"options": { "path": "a.log", "async": "false", "rotate": "false" },
"type": "file"
}
}
})JSON");
handlerConfig.set_file_path("");
config.set_handlers({handlerConfig});
s = generateLogConfig(config, "x");
checkConfig(s, R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
},
"handlers": {
"normal": {
"options": { "path": "x.log", "async": "false", "rotate": "false" },
"type": "file"
}
}
})JSON");
handlerConfig.set_start_level(folly::LogLevel::INFO);
config.set_handlers({handlerConfig});
s = generateLogConfig(config, "x");
checkConfig(s, R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
},
"handlers": {
"normal": {
"options": { "level": "INFO", "path": "x.log", "async": "false", "rotate": "false" },
"type": "file"
}
}
})JSON");
handlerConfig.set_rotate(true);
handlerConfig.set_max_files(10);
handlerConfig.set_max_file_size(100);
handlerConfig.set_rotate_on_open(true);
config.set_handlers({handlerConfig});
s = generateLogConfig(config, "x");
checkConfig(s, R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"] }
},
"handlers": {
"normal": {
"options": {
"level": "INFO",
"path": "x.log",
"async": "false",
"rotate": "true",
"max_files": "10",
"max_file_size": "100",
"rotate_on_open": "true"
},
"type": "file"
}
}
})JSON");
}
TEST(TestLogConfig, testStreamWriter) {
LogHandlerConfig handlerConfig;
handlerConfig.set_name("normal");
handlerConfig.set_writer_type(LogHandlerConfig::WriterType::STREAM);
handlerConfig.set_stream_type(LogHandlerConfig::StreamType::STDOUT);
handlerConfig.set_start_level(folly::LogLevel::INFO);
LogConfig config;
// this case does not care about other categories
config.set_handlers({handlerConfig});
checkConfig(generateLogConfig(config, "x"), R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal","err","fatal"] }
},
"handlers": {
"normal": { "options": { "level": "INFO", "stream": "stdout" }, "type": "stream" }
}
})JSON");
}
TEST(TestLogConfig, testOtherHandlers) {
std::vector<LogHandlerConfig> handlers(2);
handlers[0].set_name("normal");
handlers[0].set_writer_type(LogHandlerConfig::WriterType::STREAM);
handlers[0].set_async(true);
handlers[0].set_start_level(folly::LogLevel::MIN_LEVEL);
handlers[1].set_name("err");
handlers[1].set_writer_type(LogHandlerConfig::WriterType::FILE);
handlers[1].set_rotate(false);
handlers[1].set_async(false);
handlers[1].set_start_level(folly::LogLevel::ERR);
LogConfig config;
config.set_handlers(handlers);
checkConfig(generateLogConfig(config, "X"), R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal", "err", "fatal"]}
},
"handlers": {
"normal": {
"options": { "stream": "stderr" },
"type": "stream"
},
"err": {
"options": { "level": "ERR", "path": "X.err.log", "async": "false", "rotate": "false" },
"type": "file"
}
}
})JSON");
}
TEST(TestLogConfig, testEventLog) {
std::vector<LogCategoryConfig> categories(2);
std::vector<LogHandlerConfig> handlers(2);
categories[0].set_categories({"."});
categories[0].set_level(folly::LogLevel::INFO);
categories[0].set_handlers({"normal", "err", "fatal"});
categories[1].set_categories({"eventlog"});
categories[1].set_inherit(false);
categories[1].set_propagate(folly::LogLevel::CRITICAL);
categories[1].set_level(folly::LogLevel::INFO);
categories[1].set_handlers({"event"});
handlers[0].set_name("normal");
handlers[0].set_writer_type(LogHandlerConfig::WriterType::STREAM);
handlers[0].set_start_level(folly::LogLevel::MIN_LEVEL);
handlers[1].set_name("err");
handlers[1].set_writer_type(LogHandlerConfig::WriterType::EVENT);
handlers[1].set_rotate(false);
handlers[1].set_async(false);
handlers[1].set_start_level(folly::LogLevel::ERR);
LogConfig config;
config.set_categories(categories);
config.set_handlers(handlers);
checkConfig(generateLogConfig(config, "X"), R"JSON({
"categories": {
".": { "level": "INFO", "inherit": true, "propagate": "NONE", "handlers": ["normal","err","fatal"] },
"eventlog": { "level": "INFO", "inherit": false, "propagate": "CRITICAL", "handlers": ["event"] }
},
"handlers": {
"normal": {
"type": "stream",
"options": { "stream": "stderr" }
},
"err": {
"type": "event",
"options": { "level": "ERR", "path": "X.err.log", "async": "false", "rotate": "false" }
}
}
})JSON");
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,244 @@
#include <chrono>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/logging/xlog.h>
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <ratio>
#include <thread>
#include <vector>
#include "common/monitor/Monitor.h"
#include "common/monitor/Recorder.h"
#include "common/monitor/Sample.h"
namespace hf3fs::monitor {
bool checkRecorderHasTag(const Recorder &rec, const TagSet &tag) { return rec.map_.find(tag) != rec.map_.end(); }
void printSamples(const std::vector<Sample> &samples) {
XLOGF(INFO, "{} Samples:", samples.size());
for (auto &sample : samples) {
if (sample.isNumber()) {
XLOGF(INFO, "\t{}: {}", sample.name, sample.number());
} else if (sample.isDistribution()) {
auto &dist = sample.dist();
XLOGF(INFO,
"\t{}: cnt {} sum {} min {} p50 {} p90 {} p95 {} p99 {} max {}",
sample.name,
dist.cnt,
dist.sum,
dist.min,
dist.p50,
dist.p90,
dist.p95,
dist.p99,
dist.max);
}
}
}
namespace test {
size_t countMatchedSamples(const std::string &counterPrefix, const std::vector<Sample> &samples) {
size_t sampleSize = 0;
for (const auto &s : samples)
if (s.name.starts_with(counterPrefix)) sampleSize++;
return sampleSize;
}
size_t filterSamples(const std::string &counterPrefix, std::vector<Sample> &samples) {
std::vector<Sample> matchedSamples;
for (const auto &s : samples)
if (s.name.starts_with(counterPrefix)) matchedSamples.push_back(s);
matchedSamples.swap(samples);
return samples.size();
}
TEST(TestMonitor, StartAndStop) {
Monitor::Config config;
Monitor::start(config);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Monitor::stop();
}
TEST(TestMonitor, CountRecorder) {
auto &collector = Monitor::getDefaultInstance().getCollector();
CountRecorder testAdder("test_addr");
CountRecorder tagAdderA("tag_adder", monitor::TagSet{{"tag", "a"}});
CountRecorder tagAdderB("tag_adder", monitor::TagSet{{"tag", "b"}});
constexpr auto N = 8;
constexpr auto M = 1000000;
std::vector<std::thread> threads(N);
for (auto &thread : threads) {
thread = std::thread([&] {
for (auto m = 0; m < M; ++m) {
testAdder.addSample(1);
}
});
}
for (auto &thread : threads) {
thread.join();
}
std::vector<Sample> samples;
testAdder.collect(samples);
ASSERT_EQ(countMatchedSamples(testAdder.name(), samples), 1);
filterSamples(testAdder.name(), samples);
ASSERT_EQ(samples.front().number(), N * M);
// TODO_221013
monitor::TagSet recorder_tag_a;
recorder_tag_a.addTag("tag", "a");
monitor::TagSet recorder_tag_b;
recorder_tag_b.addTag("tag", "b");
testAdder.addSample(1, recorder_tag_a);
testAdder.addSample(1, recorder_tag_b);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(testAdder.name(), samples), 2);
ASSERT_TRUE(checkRecorderHasTag(testAdder, recorder_tag_a));
ASSERT_TRUE(checkRecorderHasTag(testAdder, recorder_tag_b));
testAdder.addSample(1, recorder_tag_b);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(testAdder.name(), samples), 1);
printSamples(samples);
ASSERT_FALSE(checkRecorderHasTag(testAdder, recorder_tag_a));
ASSERT_TRUE(checkRecorderHasTag(testAdder, recorder_tag_b));
testAdder.addSample(1, recorder_tag_a);
testAdder.addSample(1, recorder_tag_b);
samples.clear();
collector.collectAll(0, samples, false);
printSamples(samples);
monitor::TagSet recorder_tag_1{{"anothertag", "1"}};
monitor::TagSet recorder_tag_2{{"anothertag", "2"}};
tagAdderA.addSample(1, recorder_tag_1);
tagAdderA.addSample(1, recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagAdderA.name(), samples), 2);
tagAdderB.addSample(1, recorder_tag_1);
tagAdderB.addSample(1, recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagAdderB.name(), samples), 2);
tagAdderA.addSample(1, recorder_tag_1);
tagAdderB.addSample(1, recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagAdderA.name(), samples), 2);
tagAdderA.addSample(1, recorder_tag_1);
tagAdderA.addSample(1, recorder_tag_2);
tagAdderB.addSample(1, recorder_tag_1);
tagAdderB.addSample(1, recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagAdderA.name(), samples), 4);
}
TEST(TestMonitor, LatencyRecorder) {
auto &collector = Monitor::getDefaultInstance().getCollector();
LatencyRecorder testLatency("test_latency");
LatencyRecorder tagLatencyA("tag_latency", monitor::TagSet{{"tag", "a"}});
LatencyRecorder tagLatencyB("tag_latency", monitor::TagSet{{"tag", "b"}});
constexpr auto N = 10;
constexpr auto M = 1000000;
std::vector<std::thread> threads(N);
int cnt = 0;
for (auto &thread : threads) {
thread = std::thread([&, duration = cnt++ * M] {
for (auto m = 0; m < M; ++m) {
testLatency.addSample(std::chrono::nanoseconds(duration + m));
}
});
}
for (auto &thread : threads) {
thread.join();
}
std::vector<Sample> samples;
testLatency.collect(samples);
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 1);
auto &sample = samples.front();
ASSERT_TRUE(sample.isDistribution());
ASSERT_EQ(sample.dist().cnt, N * M);
ASSERT_NEAR(sample.dist().mean(), N * M / 2.0, 1e1);
ASSERT_NEAR(sample.dist().min, 0, 1e-4);
ASSERT_NEAR(sample.dist().max, N * M - 1.0, 1e-4);
ASSERT_NEAR(sample.dist().p90, N * M * 0.9, 1e4);
ASSERT_NEAR(sample.dist().p99, N * M * 0.99, 1e4);
samples.clear();
testLatency.collect(samples);
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 0);
// TODO_221013
monitor::TagSet recorder_tag_a;
recorder_tag_a.addTag("tag", "a");
monitor::TagSet recorder_tag_b;
recorder_tag_b.addTag("tag", "b");
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_a);
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_b);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 2);
printSamples(samples);
ASSERT_TRUE(checkRecorderHasTag(testLatency, recorder_tag_a));
ASSERT_TRUE(checkRecorderHasTag(testLatency, recorder_tag_b));
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_b);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(testLatency.name(), samples), 1);
ASSERT_FALSE(checkRecorderHasTag(testLatency, recorder_tag_a));
ASSERT_TRUE(checkRecorderHasTag(testLatency, recorder_tag_b));
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_a);
testLatency.addSample(std::chrono::nanoseconds(1), recorder_tag_b);
samples.clear();
collector.collectAll(0, samples, false);
printSamples(samples);
monitor::TagSet recorder_tag_1{{"anothertag", "1"}};
monitor::TagSet recorder_tag_2{{"anothertag", "2"}};
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagLatencyA.name(), samples), 2);
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagLatencyB.name(), samples), 2);
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagLatencyA.name(), samples), 2);
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
tagLatencyA.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_1);
tagLatencyB.addSample(std::chrono::nanoseconds(1), recorder_tag_2);
samples.clear();
collector.collectAll(0, samples, true);
ASSERT_EQ(countMatchedSamples(tagLatencyA.name(), samples), 4);
}
TEST(TestMonitor, DISABLED_abort_on_duplicate) {
LatencyRecorder recorder1("tag_latency", monitor::TagSet{{"tag", "a"}});
LatencyRecorder recorder2("tag_latency", monitor::TagSet{{"tag", "b"}});
// abort here.
LatencyRecorder recorder3("tag_latency", monitor::TagSet{{"tag", "b"}});
}
} // namespace test
} // namespace hf3fs::monitor

View File

@@ -0,0 +1,66 @@
#include <chrono>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/logging/xlog.h>
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <ratio>
#include <thread>
#include <vector>
#include "common/monitor/Monitor.h"
#include "common/monitor/Recorder.h"
namespace hf3fs::monitor {
namespace test {
// const std::string kDatabase = "unit_test";
// All test disabled because they need a running monitor collector server.
TEST(TestMonitorCollector, DISABLED_MonitorCollectorStartAndStop) {
try {
monitor::TagSet recorder_tag_a;
recorder_tag_a.addTag("tag", "this_is_a_tag");
CountRecorder testAdder("test_addr_count", recorder_tag_a);
testAdder.addSample(1);
Monitor::Config config_;
config_.set_reporters_length(1);
config_.reporters(0).set_type("monitor_collector");
config_.reporters(0).monitor_collector().set_remote_ip("1.2.3.4:5678");
Monitor::start(config_);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Monitor::stop();
} catch (error_t e) {
ASSERT_TRUE(false);
}
}
TEST(TestMonitorCollector, DISABLED_CountRecorderWithMonitorCollector) {
try {
monitor::TagSet recorder_tag_a;
recorder_tag_a.addTag("tag", "a");
monitor::TagSet recorder_tag_b;
recorder_tag_b.addTag("tag", "b");
CountRecorder testAdder("test_addr_count");
testAdder.addSample(1, recorder_tag_a);
testAdder.addSample(1, recorder_tag_a);
testAdder.addSample(1, recorder_tag_b);
Monitor::Config config_;
config_.set_reporters_length(1);
config_.reporters(0).set_type("monitor_collector");
config_.reporters(0).monitor_collector().set_remote_ip("1.2.3.4:5678");
Monitor::start(config_);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Monitor::stop();
} catch (error_t e) {
ASSERT_TRUE(false);
}
ASSERT_TRUE(true);
}
} // namespace test
} // namespace hf3fs::monitor

View File

@@ -0,0 +1,62 @@
#include <chrono>
#include <gtest/gtest.h>
#include "common/monitor/Monitor.h"
namespace hf3fs::monitor::test {
namespace {
/*
TEST(TestTaosClient, Normal) {
constexpr auto kInsertCount = 10000;
const std::string kDatabase = "unittest";
TaosClient::Config config;
config.set_db(kDatabase);
TaosClient client(config);
ASSERT_TRUE(client.init());
ASSERT_TRUE(client.query(fmt::format("drop database if exists {};", kDatabase)));
ASSERT_TRUE(client.query(fmt::format("create database {} precision 'us' update 1 keep 30;", kDatabase)));
auto start = std::chrono::steady_clock::now();
std::vector<Sample> samples;
samples.reserve(kInsertCount);
for (int64_t i = 0; i < kInsertCount; ++i) {
TagSet sample_tag;
samples.emplace_back(Sample{"count", sample_tag, UtcClock::now() + std::chrono::microseconds(i), i});
}
ASSERT_TRUE(client.commit(samples));
auto elapsed = std::chrono::steady_clock::now() - start;
fmt::print("Write {} count samples, takes {}ms\n",
kInsertCount,
std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
start = std::chrono::steady_clock::now();
samples.clear();
samples.reserve(kInsertCount);
for (uint64_t i = 0; i < kInsertCount; ++i) {
Distribution dist;
dist.cnt = i + 1;
dist.sum = (i + 1) * 50;
dist.min = 0;
dist.max = 100;
dist.p50 = 50;
dist.p90 = 90;
dist.p95 = 95;
dist.p99 = 99;
TagSet sample_tag;
samples.emplace_back(Sample{"latency", sample_tag, UtcClock::now() + std::chrono::microseconds(i), dist});
}
ASSERT_TRUE(client.commit(samples));
elapsed = std::chrono::steady_clock::now() - start;
fmt::print("Write {} latency samples, takes {}ms\n",
kInsertCount,
std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
client.stop();
}
*/
} // namespace
} // namespace hf3fs::monitor::test

32
tests/common/net/Echo.h Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include "common/net/ib/RDMABuf.h"
#include "common/serde/Service.h"
namespace hf3fs::net::test {
struct EchoReq {
SERDE_STRUCT_FIELD(val, std::string{});
SERDE_STRUCT_FIELD(rdma_bufs, std::vector<RDMARemoteBuf>{});
};
struct EchoRsp {
SERDE_STRUCT_FIELD(val, std::string{});
};
struct HelloReq {
SERDE_STRUCT_FIELD(val, std::string{});
};
struct HelloRsp {
SERDE_STRUCT_FIELD(val, std::string{});
SERDE_STRUCT_FIELD(idx, uint32_t{});
};
SERDE_SERVICE(Echo, 86) {
SERDE_SERVICE_METHOD(echo, 1, EchoReq, EchoRsp);
SERDE_SERVICE_METHOD(hello, 2, HelloReq, HelloRsp);
SERDE_SERVICE_METHOD(fail, 3, HelloReq, HelloRsp);
};
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,441 @@
#include <chrono>
#include <folly/Random.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/executors/IOThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/logging/xlog.h>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include <memory>
#include <tuple>
#include <utility>
#include <vector>
#include "common/net/Client.h"
#include "common/net/Listener.h"
#include "common/net/Server.h"
#include "common/net/ib/IBDevice.h"
#include "common/net/sync/Client.h"
#include "common/serde/ClientContext.h"
#include "common/utils/Address.h"
#include "common/utils/Coroutine.h"
#include "tests/GtestHelpers.h"
#include "tests/common/net/Echo.h"
#include "tests/common/net/ib/SetupIB.h"
DEFINE_uint32(test_echo_loops, 1000, "run echo RPC times per coroutine in TestEcho.MultiThreads");
DEFINE_uint32(test_echo_threads, 8, "threads number in TestEcho.MultiThreads");
namespace hf3fs::net::test {
namespace {
class EchoServiceImpl : public serde::ServiceWrapper<EchoServiceImpl, Echo> {
public:
CoTryTask<EchoRsp> echo(serde::CallContext &ctx, const EchoReq &req) {
EchoRsp rsp;
rsp.val = req.val;
co_return rsp;
}
CoTryTask<HelloRsp> hello(serde::CallContext &ctx, const HelloReq &req) {
HelloRsp rsp;
rsp.val = "Hello, " + req.val;
rsp.idx = ++idx_;
co_return rsp;
}
CoTryTask<HelloRsp> fail(serde::CallContext &ctx, const HelloReq &) {
fmt::print("Request from {}\n", ctx.transport()->describe());
co_return makeError(RPCCode::kInvalidMessageType, "failed");
}
private:
uint32_t idx_ = 0;
};
static_assert(EchoServiceImpl::kServiceID == 86, "check service id failed");
using namespace std::chrono_literals;
class TestEcho : public testing::TestWithParam<std::tuple<Address::Type, bool>> {
public:
static void SetUpTestSuite() { SetupIB::SetUpTestSuite(); }
void TearDown() override {
client_.stopAndJoin();
server_.stopAndJoin();
}
Address serverAddr() const { return server_.groups().front()->addressList().front(); }
protected:
Client::Config clientConfig_;
bool initClientConfig = [this] {
auto rwInEventThread = std::get<1>(GetParam());
clientConfig_.io_worker().set_read_write_rdma_in_event_thread(rwInEventThread);
clientConfig_.io_worker().set_read_write_tcp_in_event_thread(rwInEventThread);
return true;
}();
Client client_{clientConfig_};
Server::Config serverConfig_;
bool initServerConfig = [this] {
serverConfig_.groups(0).set_network_type(std::get<0>(GetParam()));
auto rwInEventThread = std::get<1>(GetParam());
serverConfig_.groups(0).io_worker().set_read_write_rdma_in_event_thread(rwInEventThread);
serverConfig_.groups(0).io_worker().set_read_write_tcp_in_event_thread(rwInEventThread);
return true;
}();
Server server_{serverConfig_};
};
TEST_P(TestEcho, RequestAndResponse) {
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
client_.start();
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
{
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req);
if (result.hasError()) {
std::cout << result.error().describe() << std::endl;
}
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, "foo");
}
{
HelloReq req;
req.val = "foo";
auto result = co_await client.hello(ctx, req);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, "Hello, foo");
CO_ASSERT_EQ(result->idx, 1);
result = co_await client.hello(ctx, req);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->idx, 2);
}
{
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, "foo");
}
{
HelloReq req;
req.val = "foo";
auto result = co_await client.fail(ctx, req);
CO_ASSERT_TRUE(result.hasError());
CO_ASSERT_EQ(result.error().code(), RPCCode::kInvalidMessageType);
CO_ASSERT_EQ(result.error().message(), "failed");
}
}));
}
TEST_P(TestEcho, AddrType) {
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
client_.start();
auto addr = serverAddr();
addr.type = addr.isTCP() ? Address::RDMA : Address::TCP;
auto ctx = client_.serdeCtx(addr);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
{
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req);
if (addr.isTCP()) {
CO_ASSERT_TRUE(result.hasValue());
} else {
CO_ASSERT_FALSE(result.hasValue());
}
}
}));
}
TEST_P(TestEcho, TimeoutStopped) {
// Timeout caused by a stopped service
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
client_.start();
net::UserRequestOptions options{};
options.timeout = 100_ms;
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
server_.stopAndJoin();
{
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req, &options);
CO_ASSERT_TRUE(result.hasError());
CO_ASSERT_EQ(result.error().code(), RPCCode::kTimeout);
}
}));
}
TEST_P(TestEcho, WithTimestamp) {
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
client_.start();
auto ctx = client_.serdeCtx(serverAddr());
serde::Timestamp timestamp;
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req, nullptr, &timestamp);
if (result.hasError()) {
XLOGF(CRITICAL, "error: {}", result.error().describe());
}
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, "foo");
}));
ASSERT_NE(timestamp.serverLatency(), Duration{});
}
TEST_P(TestEcho, Frozen) {
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
client_.start();
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
server_.setFrozen(true);
{
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req);
CO_ASSERT_TRUE(result.hasError());
CO_ASSERT_EQ(result.error().code(), RPCCode::kTimeout);
}
server_.setFrozen(false);
{
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, "foo");
}
}));
}
TEST_P(TestEcho, SendFailed) {
client_.start();
net::UserRequestOptions options;
options.timeout = 1_s;
auto ctx = client_.serdeCtx(Address{0});
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
{
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req, &options);
CO_ASSERT_TRUE(result.hasError());
CO_ASSERT_EQ(result.error().code(), RPCCode::kSendFailed);
}
}));
}
TEST_P(TestEcho, LargeMessage) {
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
client_.start();
net::UserRequestOptions options;
options.timeout = 5_s;
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
EchoReq req;
req.val = std::string(10_MB + 1, 'A');
auto result = co_await client.echo(ctx, req, &options);
if (result.hasError()) {
XLOGF(CRITICAL, "error: {}", result.error().describe());
}
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, req.val);
}));
}
TEST_P(TestEcho, CompressedLargeMessage) {
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
client_.start();
net::UserRequestOptions options;
options.timeout = 5_s;
options.compression = {1, 1_KB};
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
EchoReq req;
req.val = std::string(10_MB + 1, 'A');
auto result = co_await client.echo(ctx, req, &options);
if (result.hasError()) {
XLOGF(CRITICAL, "error: {}", result.error().describe());
}
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, req.val);
}));
}
TEST_P(TestEcho, MultiThreads) {
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server_.setup());
ASSERT_TRUE(server_.start());
// some CI machine enable kmemleak test, may case long delay.
clientConfig_.set_default_timeout(10000_ms);
client_.start();
net::UserRequestOptions options;
options.timeout = 5_s;
auto ctx = client_.serdeCtx(serverAddr());
auto testCoro = [&]() -> CoTask<bool> {
for (size_t i = 0; i < FLAGS_test_echo_loops; i++) {
Echo<> client;
EchoReq req;
req.val = std::to_string(folly::Random::rand32());
auto result = co_await client.echo(ctx, req, &options);
if (result.hasError()) {
XLOGF(CRITICAL, "error: {}", result.error().describe());
co_return false;
}
EXPECT_EQ(result->val, req.val);
}
co_return true;
};
for (size_t numThreads = 2; numThreads <= FLAGS_test_echo_threads; numThreads *= 2) {
folly::CPUThreadPoolExecutor exec(numThreads);
std::vector<folly::SemiFuture<bool>> tasks;
for (size_t coroId = 0; coroId < 16 * numThreads; coroId++) {
tasks.emplace_back(testCoro().scheduleOn(folly::Executor::getKeepAliveToken(exec)).start());
}
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
std::vector<bool> results = co_await folly::coro::collectAllRange(std::move(tasks));
CO_ASSERT_TRUE(std::all_of(results.begin(), results.end(), [](auto ok) { return ok; }));
}));
exec.join();
}
}
INSTANTIATE_TEST_SUITE_P(TestEcho,
TestEcho,
testing::Combine(testing::Values(Address::RDMA, Address::TCP, Address::UNIX),
testing::Values(true, false)));
TEST(TestEchoSync, Normal) {
std::array<Address::Type, 4> networks{Address::LOCAL, Address::TCP /*, Address::IPoIB */};
for (auto network : networks) {
Server::Config serverConfig;
serverConfig.groups(0).set_network_type(network);
Server server_{serverConfig};
server_.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_OK(server_.setup());
auto initResult = server_.start();
if (!initResult) {
fmt::print("init failed: {}\n", initResult.error().describe());
ASSERT_TRUE(initResult);
}
auto serverAddr = server_.groups().front()->addressList().front();
sync::Client::Config clientConfig;
sync::Client client_{clientConfig};
auto ctx = client_.serdeCtx(serverAddr);
Echo<> client;
EchoReq req;
req.val = "foo";
auto result = client.echoSync(ctx, req);
if (result.hasError()) {
std::cout << result.error().describe() << std::endl;
}
ASSERT_FALSE(result.hasError());
auto &rsp = result.value();
ASSERT_EQ(rsp.val, "foo");
req.val = "bar";
result = client.echoSync(ctx, req);
if (result.hasError()) {
std::cout << result.error().describe() << std::endl;
}
ASSERT_FALSE(result.hasError());
ASSERT_EQ(rsp.val, "bar");
}
}
TEST(TestEchoListener, Normal) {
Server::Config serverConfig;
serverConfig.groups(0).set_network_type(Address::TCP);
serverConfig.groups(0).listener().set_listen_port(folly::Random::rand32(10000, 20000)); // same port.
Server server1{serverConfig};
Server server2{serverConfig};
ASSERT_OK(server1.setup());
ASSERT_FALSE(server2.setup());
ASSERT_OK(server1.start());
ASSERT_FALSE(server2.start());
}
TEST(TestEchoListener, StartAndStop) {
constexpr auto N = 100;
for (auto i = 0; i < N; ++i) {
Server::Config serverConfig;
serverConfig.groups(0).set_network_type(Address::TCP);
Server server{serverConfig};
ASSERT_OK(server.setup());
server.stopAndJoin();
}
}
TEST(TestEchoListener, DomainSocket) {
Server::Config serverConfig;
serverConfig.groups(0).set_network_type(Address::UNIX);
serverConfig.groups(0).listener().set_listen_port(folly::Random::rand32(10000, 20000)); // same port.
Server server{serverConfig};
ASSERT_OK(server.setup());
ASSERT_OK(server.start());
std::this_thread::sleep_for(1_s);
server.stopAndJoin();
}
} // namespace
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,58 @@
#include <gtest/gtest.h>
#include "common/net/EventLoop.h"
#include "common/net/Network.h"
#include "common/utils/FdWrapper.h"
namespace hf3fs::net::test {
namespace {
TEST(TestEventLoop, Normal) {
auto eventLoop = EventLoop::create();
ASSERT_TRUE(eventLoop->start());
eventLoop->stopAndJoin();
}
TEST(TestEventLoop, AddObject) {
auto eventLoop = EventLoop::create();
ASSERT_TRUE(eventLoop->start());
class PipeReader : public EventLoop::EventHandler {
public:
PipeReader(int fd)
: fd_(fd) {}
int fd() const final { return fd_; }
void handleEvents(uint32_t epollEvents) final {
ASSERT_TRUE(epollEvents & EPOLLIN);
uint32_t val;
ASSERT_EQ(::read(fd_, &val, sizeof(val)), sizeof(val));
val_ = val;
}
auto &val() const { return val_; }
private:
FdWrapper fd_;
std::atomic<uint32_t> val_{0};
};
int p[2];
ASSERT_EQ(::pipe(p), 0);
auto pipeReader = std::make_shared<PipeReader>(p[0]);
ASSERT_TRUE(eventLoop->add(pipeReader, EPOLLIN | EPOLLET));
ASSERT_EQ(pipeReader->val().load(), 0);
uint32_t val = rand();
ASSERT_EQ(::write(p[1], &val, sizeof(val)), sizeof(val));
while (pipeReader->val().load() == 0) {
std::this_thread::sleep_for(10ms);
}
ASSERT_EQ(pipeReader->val().load(), val);
ASSERT_TRUE(eventLoop->remove(pipeReader.get()));
std::this_thread::sleep_for(10ms);
}
} // namespace
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,25 @@
#include <folly/net/NetworkSocket.h>
#include <gtest/gtest.h>
#include "common/net/IOWorker.h"
#include "common/net/Network.h"
#include "common/net/Processor.h"
#include "common/net/Transport.h"
namespace hf3fs::net {
TEST(TestIOWorker, Normal) {
CPUExecutorGroup procExecutor(2, "");
CPUExecutorGroup ioExecutor(2, "");
folly::IOThreadPoolExecutor connExecutor(2);
serde::Services serdeServices;
Processor::Config processorConfig{};
Processor processor(serdeServices, procExecutor, processorConfig);
IOWorker::Config ioWorkerConfig{};
IOWorker ioWorker(processor, ioExecutor, connExecutor, ioWorkerConfig);
ASSERT_TRUE(ioWorker.start("Test"));
}
} // namespace hf3fs::net

View File

@@ -0,0 +1,97 @@
#include <atomic>
#include <cstdint>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/futures/Future.h>
#include <gtest/gtest.h>
#include "common/net/Client.h"
#include "common/net/Network.h"
#include "common/net/Server.h"
#include "common/serde/Service.h"
#include "tests/common/net/Echo.h"
#include "tests/common/net/ib/SetupIB.h"
namespace hf3fs::net::test {
namespace {
class EchoServiceImpl : public serde::ServiceWrapper<EchoServiceImpl, Echo> {
public:
CoTryTask<EchoRsp> echo(serde::CallContext &ctx, const EchoReq &) {
co_await folly::coro::sleep(50ms);
co_return EchoRsp{};
}
CoTryTask<HelloRsp> hello(serde::CallContext &ctx, const HelloReq &) { co_return HelloRsp{}; }
CoTryTask<HelloRsp> fail(serde::CallContext &ctx, const HelloReq &) { co_return HelloRsp{}; }
Status onError(Status status) {
++error_;
return status;
}
uint32_t error() { return error_; }
private:
std::atomic<uint32_t> error_ = 0;
};
} // namespace
class TestProcessor : public SetupIB {};
TEST_F(TestProcessor, TooManyProcessingRequests) {
constexpr auto kMaxProcessingRequestsNum = 10;
constexpr auto kConcurrentRequestsNum = 16;
Server::Config serverConfig;
serverConfig.groups(0).processor().set_max_processing_requests_num(kMaxProcessingRequestsNum);
Server server(serverConfig);
ASSERT_TRUE(server.setup());
auto impl = std::make_unique<EchoServiceImpl>();
auto ptr = impl.get();
server.addSerdeService(std::move(impl));
ASSERT_TRUE(server.start());
ASSERT_EQ(ptr->error(), 0);
Client::Config clientConfig;
Client client{clientConfig};
client.start();
auto ctx = client.serdeCtx(server.groups().front()->addressList().front());
folly::coro::blockingWait([&]() -> CoTask<void> {
Echo<> client;
std::vector<folly::SemiFuture<Result<EchoRsp>>> requests;
requests.reserve(kConcurrentRequestsNum);
EchoReq echoReq{};
for (auto i = 0; i < kConcurrentRequestsNum; ++i) {
requests.push_back(client.echo(ctx, echoReq).semi());
}
auto results = co_await folly::collectAll(std::move(requests));
auto succ = 0;
auto fail = 0;
for (auto &result : results) {
auto &rsp = *result;
if (rsp.hasError()) {
++fail;
CO_ASSERT_EQ(rsp.error().code(), RPCCode::kRequestRefused);
} else {
++succ;
}
}
CO_ASSERT_GE(succ, kMaxProcessingRequestsNum);
CO_ASSERT_GT(fail, 0);
CO_ASSERT_LE(fail, kConcurrentRequestsNum - kMaxProcessingRequestsNum);
CO_ASSERT_NE(ptr->error(), 0);
CO_ASSERT_EQ(ptr->error(), fail);
auto [rsp, _] = co_await folly::coro::collectAll(client.echo(ctx, EchoReq{}), [&]() -> CoTask<void> {
co_await folly::coro::sleep(10ms);
server.stopAndJoin();
}());
}());
}
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,51 @@
#include "common/net/Client.h"
#include "common/net/RDMAControl.h"
#include "common/net/RequestOptions.h"
#include "common/net/Server.h"
#include "common/serde/ClientContext.h"
#include "tests/GtestHelpers.h"
#include "tests/common/net/ib/SetupIB.h"
namespace hf3fs::net::test {
namespace {
struct RDMAControlService : serde::ServiceWrapper<RDMAControlService, RDMAControl> {
CoTryTask<RDMATransmissionRsp> apply(serde::CallContext &ctx, const RDMATransmissionReq &req) {
auto &tr = ctx.transport();
if (!tr->isRDMA()) {
co_return makeError(StatusCode::kInvalidArg);
}
serde::ClientContext clientCtx(tr);
auto result = co_await RDMAControl<>::apply(clientCtx, req);
co_return result;
}
};
TEST(TestRDMAControl, Normal) {
SetupIB::SetUpTestSuite();
net::Server::Config serverConfig;
net::Server server_{serverConfig};
ASSERT_OK(server_.setup());
ASSERT_OK(server_.start());
ASSERT_OK(server_.addSerdeService(std::make_unique<RDMAControlService>()));
net::Client::Config clientConfig;
net::Client client_{clientConfig};
ASSERT_OK(client_.start());
auto ctx = client_.serdeCtx(server_.groups().front()->addressList().front());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
{
RDMATransmissionReq req;
auto result = co_await RDMAControl<>::apply(ctx, req);
if (result.hasError()) {
std::cout << result.error().describe() << std::endl;
}
CO_ASSERT_OK(result);
}
}));
}
} // namespace
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,62 @@
#include <chrono>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <gtest/gtest.h>
#include <thread>
#include "common/net/Waiter.h"
#include "common/utils/Coroutine.h"
namespace hf3fs::net::test {
namespace {
using namespace std::chrono_literals;
TEST(TestTimer, Normal) {
folly::coro::blockingWait([&]() -> CoTask<void> {
Waiter::Item item;
auto uuid = Waiter::instance().bind(item);
auto start = std::chrono::steady_clock::now();
Waiter::instance().schedule(uuid, std::chrono::milliseconds(50));
co_await item.baton;
auto elapsed = std::chrono::steady_clock::now() - start;
CO_ASSERT_TRUE(30ms <= elapsed && elapsed <= 70ms);
CO_ASSERT_EQ(item.status.code(), RPCCode::kTimeout);
}());
}
TEST(TestTimer, TwoUUIDs) {
folly::coro::blockingWait([&]() -> CoTask<void> {
Waiter::Item item1;
auto uuid1 = Waiter::instance().bind(item1);
Waiter::Item item2;
auto uuid2 = Waiter::instance().bind(item2);
auto start = std::chrono::steady_clock::now();
Waiter::instance().schedule(uuid1, std::chrono::milliseconds(50));
Waiter::instance().schedule(uuid2, std::chrono::milliseconds(80));
co_await item1.baton;
co_await item2.baton;
auto elapsed = std::chrono::steady_clock::now() - start;
CO_ASSERT_TRUE(60ms <= elapsed);
CO_ASSERT_TRUE(elapsed <= 100ms);
}());
}
TEST(TestTimer, TimeoutBeforeWait) {
folly::coro::blockingWait([&]() -> CoTask<void> {
Waiter::Item item;
auto uuid = Waiter::instance().bind(item);
Waiter::instance().schedule(uuid, std::chrono::milliseconds(0));
std::this_thread::sleep_for(10ms);
co_await item.baton;
}());
}
} // namespace
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,22 @@
#pragma once
#include <folly/logging/xlog.h>
#include <gtest/gtest.h>
#include "common/net/ib/IBDevice.h"
#include "common/utils/ConfigBase.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::net::test {
class SetupIB : public ::testing::Test {
public:
static void SetUpTestSuite() {
static IBConfig config;
auto ib = IBManager::start(config);
XLOGF_IF(FATAL, ib.hasError(), "IBManager start failed, result {}", ib.error());
ASSERT_FALSE(IBDevice::all().empty());
}
};
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,263 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <fmt/compile.h>
#include <fmt/core.h>
#include <folly/Conv.h>
#include <folly/IPAddressV4.h>
#include <folly/Random.h>
#include <folly/ScopeGuard.h>
#include <folly/String.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/futures/detail/Types.h>
#include <folly/logging/xlog.h>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include <infiniband/verbs.h>
#include <iostream>
#include <set>
#include <string>
#include <string_view>
#include <thread>
#include <unistd.h>
#include <vector>
#include "SetupIB.h"
#include "common/net/IfAddrs.h"
#include "common/net/ib/IBConnect.h"
#include "common/net/ib/IBDevice.h"
#include "common/utils/Duration.h"
#include "common/utils/MagicEnum.hpp"
#include "common/utils/UtcTime.h"
#include "tests/GtestHelpers.h"
DEFINE_uint64(ibdev_reg_mem_size, 1ull << 30, "Total memory size to register.");
DEFINE_uint32(ibdev_reg_mem_ms, 100, "Time to run reg memory test for each case.");
DEFINE_uint64(reg_gb, 1, "reg memory size.");
DEFINE_bool(reg_odp, false, "odp");
DEFINE_bool(reg_releaxed_ording, false, "releaxed ording");
DEFINE_uint32(reg_split, 1, "reg split large memory");
namespace hf3fs::net {
class TestIBDevice : public test::SetupIB {};
TEST_F(TestIBDevice, IfAddrs) {
auto result = IfAddrs::load();
ASSERT_TRUE(!result.hasError());
for (auto [name, addr] : *result) {
fmt::print("{} -> ip {}, mask {}, up {}\n", name, addr.ip.str(), addr.mask, addr.up);
}
}
TEST_F(TestIBDevice, Normal) {
for (auto &dev : IBDevice::all()) {
fmt::print("dev: {}\n", dev->name());
ASSERT_NE(dev->ports().size(), 0);
ASSERT_NE(dev->context(), nullptr);
for (size_t portNum = 1; portNum <= dev->ports().size(); portNum++) {
auto port = dev->openPort(portNum);
ASSERT_FALSE(port.hasError()) << port.error().describe();
fmt::print("\tport {}, zones {}\n", *port, fmt::join(port->zones().begin(), port->zones().end(), ";"));
ASSERT_EQ(port->dev(), dev.get());
if (port->isRoCE()) {
auto r = port->getRoCEv2Gid();
fmt::print("\tRoCE v2 GID[{}]: {}\n", r.second, r.first);
}
for (int i = 0; true; i++) {
ibv_gid gid;
auto ret = ibv_query_gid(dev->context(), port->portNum(), i, &gid);
if (ret || std::all_of(&gid.raw[0], &gid.raw[sizeof(gid)], [](auto v) { return v == 0; })) {
break;
}
auto ip = folly::IPAddressV6::fromBinary(folly::ByteRange(gid.raw, sizeof(gid.raw)));
fmt::print("\t\tGID[{}]: {}, {} {}\n", i, gid, ip.str(), ip.is6To4());
}
for (int i = 0; i < 1000; i++) {
__be16 pkey;
auto ret = ibv_query_pkey(dev->context(), portNum, i, &pkey);
if (ret != 0 || pkey == 0) {
break;
}
fmt::print("dev {}, port {}, pkey {} => {:x}\n", dev->name(), portNum, i, pkey);
}
}
}
}
TEST_F(TestIBDevice, UpdatePortStatus) {
ASSERT_FALSE(IBDevice::all().empty());
ASSERT_FALSE(IBDevice::get(0)->ports().empty());
for (auto &dev : IBDevice::all()) {
for (auto &[portNum, port] : dev->ports()) {
auto state = port.attr.rlock()->state;
// disable a port by set state to DOWN
port.attr.withWLock([](auto &attr) { attr.state = IBV_PORT_DOWN; });
// open port, then port state should be updated
auto result = dev->openPort(portNum);
ASSERT_OK(result);
ASSERT_EQ(result->attr().state, state);
ASSERT_EQ(port.attr.rlock()->state, state);
// disable a port by set state to DOWN
port.attr.withWLock([](auto &attr) { attr.state = IBV_PORT_DOWN; });
// wait sometime, period runner will update port status
auto begin = SteadyClock::now();
while (true) {
auto attr = *port.attr.rlock();
if (attr.state == state) {
break;
}
ASSERT_TRUE(SteadyClock::now() < begin + 30_s) << "port status doesn't update in 30s";
std::this_thread::sleep_for(1_s);
}
}
}
}
auto regFlags() {
auto flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
if (FLAGS_reg_odp) {
flags |= IBV_ACCESS_ON_DEMAND;
}
if (FLAGS_reg_releaxed_ording) {
flags |= IBV_ACCESS_RELAXED_ORDERING;
}
return flags;
}
TEST_F(TestIBDevice, RegMemory) {
void *mem = malloc(FLAGS_ibdev_reg_mem_size);
auto guard = folly::makeGuard([&]() { free(mem); });
auto benchmark = [&]() {
for (uint64_t bs = 4096; bs <= (16 << 20); bs *= 4) {
auto numBlock = FLAGS_ibdev_reg_mem_size / bs;
std::chrono::nanoseconds regDur(0), deregDur(0);
auto begin = std::chrono::steady_clock::now();
size_t iters = 0;
while (std::chrono::steady_clock::now() - begin < std::chrono::milliseconds(FLAGS_ibdev_reg_mem_ms)) {
std::array<ibv_mr *, 100> mrs;
auto regBegin = std::chrono::steady_clock::now();
for (size_t i = 0; i < mrs.size(); i++) {
size_t offset = (i % numBlock) * bs;
void *ptr = (uint8_t *)mem + offset;
mrs[i] = IBDevice::get(0)->regMemory(ptr, bs, regFlags());
iters++;
}
regDur += std::chrono::steady_clock::now() - regBegin;
auto deregBegin = std::chrono::steady_clock::now();
for (auto mr : mrs) {
IBDevice::get(0)->deregMemory(mr);
}
deregDur += std::chrono::steady_clock::now() - deregBegin;
}
fmt::print("bs: {:8}, reg {:192.168f} us, dereg {:192.168f} us\n",
bs,
regDur.count() / 1000.0 / iters,
deregDur.count() / 1000.0 / iters);
}
};
benchmark();
auto mr = IBDevice::get(0)->regMemory(mem, FLAGS_ibdev_reg_mem_size, regFlags());
fmt::print("Register reged memory.\n");
benchmark();
IBDevice::get(0)->deregMemory(mr);
}
TEST_F(TestIBDevice, DISABLED_RegMR) {
std::atomic_bool exit = false;
auto progress = std::jthread([&]() {
while (!exit) {
sleep(5);
XLOGF(INFO, "still alive");
}
});
auto memsize = FLAGS_reg_gb * (1ULL << 30);
XLOGF(INFO, "malloc begin {}", memsize);
auto *mem = (uint8_t *)malloc(memsize);
XLOGF(INFO, "malloc success, {} {}", memsize, (void *)mem);
std::vector<ibv_mr *> mrs;
auto bs = memsize / FLAGS_reg_split;
XLOGF(INFO, "begin reg");
for (size_t i = 0; i < FLAGS_reg_split; i++) {
auto mr = IBDevice::get(0)->regMemory(mem + bs * i, bs, regFlags());
XLOGF(INFO, "reg get {}", (void *)mr);
ASSERT_NE(mr, nullptr) << memsize;
mrs.push_back(mr);
}
XLOGF(INFO, "dereg begin");
for (auto mr : mrs) {
IBDevice::get(0)->deregMemory(mr);
}
XLOGF(INFO, "dereg finish");
exit = true;
}
TEST_F(TestIBDevice, DISABLED_VerbsRegMR) {
int num_devices;
struct ibv_device **dev_list = ibv_get_device_list(&num_devices);
SCOPE_EXIT { ibv_free_device_list(dev_list); };
ASSERT_NE(dev_list, nullptr);
XLOGF(INFO, "Use device {}", ibv_get_device_name(dev_list[0]));
auto dev = dev_list[0];
auto ctx = ibv_open_device(dev);
ASSERT_NE(ctx, nullptr);
auto pd = ibv_alloc_pd(ctx);
ASSERT_NE(pd, nullptr);
// malloc
auto memsize = FLAGS_reg_gb * (1ULL << 30);
XLOGF(INFO, "malloc begin {}", memsize);
auto *mem = (uint8_t *)malloc(memsize);
XLOGF(INFO, "malloc success, {} {}", memsize, (void *)mem);
// ibv_reg_mr
std::vector<ibv_mr *> mrs;
auto bs = memsize / FLAGS_reg_split;
XLOGF(INFO, "reg begin");
for (size_t i = 0; i < FLAGS_reg_split; i++) {
auto mr = ibv_reg_mr(pd, mem + bs * i, bs, regFlags());
XLOGF(INFO, "reg get {}", (void *)mr);
ASSERT_NE(mr, nullptr) << memsize;
mrs.push_back(mr);
}
XLOGF(INFO, "dereg begin");
for (auto mr : mrs) {
ibv_dereg_mr(mr);
}
XLOGF(INFO, "dereg finish");
}
// on CI machine with virtualized RDMA NIC, this test will fail.
TEST_F(TestIBDevice, DISABLED_reregMR) {
static constexpr size_t kMemFlags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
for (auto i = 0; i < 100; i++) {
void *mem = malloc(4 << 10);
auto guard = folly::makeGuard([&]() { free(mem); });
auto mr1 = IBDevice::get(0)->regMemory(mem, 4 << 10, kMemFlags);
auto rkey1 = mr1->rkey;
ASSERT_EQ(IBDevice::get(0)->deregMemory(mr1), 0);
auto mr2 = IBDevice::get(0)->regMemory(mem, 4 << 10, kMemFlags);
ASSERT_NE(rkey1, mr2->rkey);
}
}
} // namespace hf3fs::net

View File

@@ -0,0 +1,105 @@
#include <chrono>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/logging/xlog.h>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include <memory>
#include <tuple>
#include <utility>
#include <vector>
#include "common/net/Client.h"
#include "common/net/Listener.h"
#include "common/net/Server.h"
#include "common/net/WriteItem.h"
#include "common/net/ib/IBDevice.h"
#include "common/net/sync/Client.h"
#include "common/serde/ClientContext.h"
#include "common/utils/Address.h"
#include "common/utils/Coroutine.h"
#include "common/utils/Result.h"
#include "gtest/gtest.h"
#include "tests/GtestHelpers.h"
#include "tests/common/net/Echo.h"
namespace hf3fs::net {
using namespace test;
class TestIBNotInitialized : public ::testing::Test {
protected:
void SetUp() override { IBManager::stop(); }
};
TEST_F(TestIBNotInitialized, Basic) {
ASSERT_FALSE(IBManager::initialized());
ASSERT_TRUE(IBDevice::all().empty());
}
class EchoServiceImpl : public serde::ServiceWrapper<EchoServiceImpl, Echo> {
public:
CoTryTask<EchoRsp> echo(serde::CallContext &ctx, const EchoReq &req) {
EchoRsp rsp;
rsp.val = req.val;
co_return rsp;
}
CoTryTask<HelloRsp> hello(serde::CallContext &ctx, const HelloReq &req) {
co_return makeError(StatusCode::kNotImplemented);
}
CoTryTask<HelloRsp> fail(serde::CallContext &ctx, const HelloReq &) {
co_return makeError(StatusCode::kNotImplemented);
}
};
TEST_F(TestIBNotInitialized, TCP) {
Server::Config serverConfig;
serverConfig.groups(0).set_network_type(Address::TCP);
Server server{serverConfig};
ASSERT_TRUE(server.setup());
server.addSerdeService(std::make_unique<EchoServiceImpl>());
ASSERT_TRUE(server.start());
Client::Config clientConfig;
Client client{clientConfig};
client.start();
auto ctx = client.serdeCtx(server.groups().front()->addressList().front());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result->val, "foo");
}));
}
TEST_F(TestIBNotInitialized, RDMAServer) {
Server::Config serverConfig;
serverConfig.groups(0).set_network_type(Address::RDMA);
Server server{serverConfig};
ASSERT_OK(server.setup());
ASSERT_ERROR(server.start(), RPCCode::kIBDeviceNotInitialized);
}
TEST_F(TestIBNotInitialized, RDMAClient) {
Client::Config clientConfig;
Client client{clientConfig};
client.start();
auto ctx = client.serdeCtx(Address::fromString("rdma://127.0.0.1:8000"));
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
Echo<> client;
EchoReq req;
req.val = "foo";
auto result = co_await client.echo(ctx, req);
CO_ASSERT_TRUE(result.hasError());
CO_ASSERT_EQ(result.error().code(), RPCCode::kSendFailed);
}));
}
} // namespace hf3fs::net

View File

@@ -0,0 +1,817 @@
#include <algorithm>
#include <atomic>
#include <bits/types/struct_iovec.h>
#include <boost/thread/barrier.hpp>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <fmt/core.h>
#include <folly/IPAddress.h>
#include <folly/IPAddressV4.h>
#include <folly/Random.h>
#include <folly/Range.h>
#include <folly/ScopeGuard.h>
#include <folly/SocketAddress.h>
#include <folly/Synchronized.h>
#include <folly/Utility.h>
#include <folly/experimental/coro/Baton.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/CurrentExecutor.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/futures/detail/Types.h>
#include <folly/hash/Checksum.h>
#include <folly/io/async/AsyncServerSocket.h>
#include <folly/io/async/EventBase.h>
#include <folly/io/coro/ServerSocket.h>
#include <folly/io/coro/Transport.h>
#include <folly/logging/xlog.h>
#include <folly/net/NetworkSocket.h>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include <infiniband/verbs.h>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <pthread.h>
#include <queue>
#include <sys/poll.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <thread>
#include <tuple>
#include <unistd.h>
#include <utility>
#include <vector>
#include "SetupIB.h"
#include "common/net/Client.h"
#include "common/net/MessageHeader.h"
#include "common/net/Network.h"
#include "common/net/Server.h"
#include "common/net/WriteItem.h"
#include "common/net/ib/IBConnect.h"
#include "common/net/ib/IBConnectService.h"
#include "common/net/ib/IBDevice.h"
#include "common/net/ib/IBSocket.h"
#include "common/net/ib/RDMABuf.h"
#include "common/serde/ClientContext.h"
#include "common/utils/Address.h"
#include "common/utils/Coroutine.h"
#include "common/utils/Duration.h"
#include "common/utils/Result.h"
#include "common/utils/UtcTime.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::net::test {
// DEFINE_uint32(gid_index, 0, "RoCE gid index.");
DEFINE_bool(ib_wait_fd, true, "IBSocket wait on fd instead of busy polling.");
DEFINE_uint64(ib_bench_mb, (4 << 10), "IBSocket benchmark send data size (mb).");
DEFINE_uint32(ib_bench_buf_kb, 0, "IBSocket benchmark send buf size");
DEFINE_uint32(ib_bench_buf_cnt, 0, "IBSocket benchmark send buf cnt");
DEFINE_uint64(ib_pingpong_loops, 10000, "IBSocket pingpong loops.");
DEFINE_uint64(ib_pingpong_size, 64, "IBSocket pingpong msg size (bytes).");
DEFINE_uint64(ib_rdma_iters, 500, "IBSocket RDMA test iters.");
struct TestUtils {
static IBSocket::Config testCfg() {
IBSocket::Config cfg;
cfg.set_retry_cnt(0);
cfg.set_buf_size(512);
cfg.set_send_buf_cnt(32);
cfg.set_buf_ack_batch(4);
cfg.set_buf_signal_batch(4);
cfg.set_max_rdma_wr(7);
cfg.set_max_rdma_wr_per_post(5);
cfg.set_max_sge(3);
return cfg;
}
static std::vector<std::jthread> poll(std::vector<IBSocket *> sockets, std::atomic<bool> &stop) {
std::vector<std::jthread> threads;
threads.reserve(sockets.size());
for (auto socket : sockets) {
threads.emplace_back([&stop, socket]() {
while (!stop) {
TestUtils::wait(*socket, 0, 100ms);
}
});
}
return threads;
}
static Result<Socket::Events> wait(IBSocket &socket, Socket::Events events, std::chrono::milliseconds timeout) {
while (true) {
if (FLAGS_ib_wait_fd && timeout.count() != 0) {
pollfd fd{
.fd = socket.fd(),
.events = POLL_IN,
.revents = 0,
};
int ret = ::poll(&fd, 1, timeout.count());
if (ret < 0 || (ret == 0 && timeout.count())) {
return makeError(RPCCode::kTimeout, fmt::format("poll ret is {}", ret));
}
}
auto result = socket.poll(0);
if (result.hasError()) {
return makeError(std::move(result.error()));
}
if (events == 0 || (result.value() & events) != 0) {
return result.value();
}
}
}
static Result<Void> send(IBSocket &socket, folly::ByteRange buf, bool poll = true) {
while (!buf.empty()) {
// try send first
auto sendResult = socket.send(buf);
RETURN_ON_ERROR(sendResult);
buf.advance(sendResult.value());
if (buf.empty()) {
break;
}
// wait writable
if (poll) {
auto waitResult = wait(socket, Socket::kEventWritableFlag, 1000ms);
RETURN_ON_ERROR(waitResult);
}
}
return socket.flush();
}
static Result<Void> send(IBSocket &socket, iovec *iovs, int iovCnts, bool poll) {
for (int i = 0; i < iovCnts; i++) {
auto result = send(socket, folly::ByteRange((uint8_t *)iovs[i].iov_base, iovs[i].iov_len), poll);
RETURN_ON_ERROR(result);
}
return Void{};
}
static Result<Void> recv(IBSocket &socket, folly::MutableByteRange buf, bool poll = true) {
if (buf.empty()) {
return Void{};
}
while (true) {
auto recvResult = socket.recv(buf);
RETURN_ON_ERROR(recvResult);
buf.advance(recvResult.value());
if (buf.empty()) {
break;
}
// wait readable
if (poll) {
auto waitResult = wait(socket, Socket::kEventReadableFlag, 1000ms);
RETURN_ON_ERROR(waitResult);
}
}
return Void{};
}
};
class TestMsg {
public:
static Result<Void> send(IBSocket &socket, size_t bytes, bool poll = true) {
TestMsg msg;
msg.bytes_ = bytes;
msg.data_.resize(bytes);
for (size_t i = 0; i < bytes; i++) {
msg.data_[i] = (uint8_t)folly::Random::rand32();
}
msg.crc32c_ = folly::crc32c(msg.data_.data(), msg.data_.size(), 0);
iovec iovs[] = {
{.iov_base = &msg.bytes_, .iov_len = sizeof(msg.bytes_)},
{.iov_base = &msg.crc32c_, .iov_len = sizeof(msg.crc32c_)},
{.iov_base = msg.data_.data(), .iov_len = msg.data_.size()},
};
return TestUtils::send(socket, iovs, sizeof(iovs) / sizeof(iovs[0]), poll);
}
static Result<Void> recv(IBSocket &socket, bool poll = true) {
TestMsg msg;
auto result =
TestUtils::recv(socket,
folly::MutableByteRange((uint8_t *)&msg.bytes_, sizeof(msg.bytes_) + sizeof(msg.crc32c_)),
poll);
RETURN_ON_ERROR(result);
msg.data_.resize(msg.bytes_);
result = TestUtils::recv(socket, folly::MutableByteRange(msg.data_.data(), msg.data_.size()), poll);
RETURN_ON_ERROR(result);
uint32_t crc32c = folly::crc32c(msg.data_.data(), msg.data_.size(), 0);
EXPECT_EQ(msg.crc32c_, crc32c);
if (msg.crc32c_ != crc32c) {
return makeError(StatusCode::kDataCorruption);
}
return Void{};
}
private:
uint32_t bytes_ = 0;
uint32_t crc32c_ = 0;
std::vector<uint8_t> data_;
};
class TestIBSocket : public SetupIB {
public:
auto &group() { return *server_.groups().front(); }
const auto &group() const { return *server_.groups().front(); }
Address serverAddr() const { return group().addressList().front(); }
void SetUp() override {
ASSERT_TRUE(server_.setup());
auto accept = [&](auto socket) { accepted_.lock()->push(std::move(socket)); };
auto service = std::make_unique<IBConnectService>(serverConfig_.groups(0).io_worker().ibsocket(), accept, []() {
return 5_s;
});
group().addSerdeService(std::move(service), Address::Type::TCP);
ASSERT_TRUE(server_.start());
client_.start();
}
void TearDown() override {
client_.stopAndJoin();
server_.stopAndJoin();
}
CoTryTask<std::pair<std::unique_ptr<IBSocket>, std::unique_ptr<IBSocket>>> connect(
IBSocket::Config &cfg,
std::chrono::milliseconds timeout = 220ms) {
// cfg.set_gid_index(FLAGS_gid_index);
auto addr = serverAddr();
Address tcpAddr(addr.ip, addr.port, Address::TCP);
auto ctx = client_.serdeCtx(tcpAddr);
auto clientSocket = std::make_unique<IBSocket>(cfg);
auto result = co_await clientSocket->connect(ctx, Duration{timeout});
CO_RETURN_ON_ERROR(result);
auto queue = accepted_.lock();
EXPECT_EQ(queue->size(), 1);
auto serverSocket = std::move(queue->front());
queue->pop();
queue.unlock();
EXPECT_FALSE(serverSocket->check().hasError());
EXPECT_FALSE(clientSocket->check().hasError());
auto serverReady = TestUtils::wait(*serverSocket, Socket::kEventWritableFlag, 500ms);
auto clientReady = TestUtils::wait(*clientSocket, Socket::kEventWritableFlag, 500ms);
EXPECT_FALSE(serverReady.hasError()) << serverReady.error().describe();
EXPECT_FALSE(clientReady.hasError()) << serverReady.error().describe();
CO_RETURN_ON_ERROR(serverReady);
CO_RETURN_ON_ERROR(clientReady);
co_return std::pair(std::move(serverSocket), std::move(clientSocket));
}
Result<std::pair<std::unique_ptr<IBSocket>, std::unique_ptr<IBSocket>>> connectSync(
IBSocket::Config &cfg,
std::chrono::milliseconds timeout = 100ms) {
return folly::coro::blockingWait(connect(cfg, timeout));
}
protected:
Client::Config clientConfig_;
Client client_{clientConfig_};
Server::Config serverConfig_;
bool initServerConfig = [this] {
serverConfig_.groups(0).set_network_type(Address::TCP);
serverConfig_.groups(0).listener().set_reuse_port(true);
return true;
}();
Server server_{serverConfig_};
folly::Synchronized<std::queue<std::unique_ptr<IBSocket>>, std::mutex> accepted_;
};
TEST_F(TestIBSocket, InvalidConfig) {
auto dev = IBDevice::get(0);
ASSERT_NE(dev, nullptr);
folly::coro::blockingWait([&]() -> CoTask<void> {
IBSocket::Config cfg;
cfg.set_max_rd_atomic(dev->attr().max_qp_rd_atom + 1);
auto result = co_await connect(cfg);
CO_ASSERT_ERROR(result, StatusCode::kInvalidConfig);
}());
}
TEST_F(TestIBSocket, ConnectTimeout) {
server_.stopAndJoin();
folly::coro::blockingWait([&]() -> CoTask<void> {
auto config = TestUtils::testCfg();
auto result = co_await connect(config);
CO_ASSERT_ERROR(result, RPCCode::kSendFailed);
}());
}
TEST_F(TestIBSocket, Connect) {
folly::coro::blockingWait([&]() -> CoTask<void> {
auto config = TestUtils::testCfg();
auto result = co_await connect(config);
CO_ASSERT_OK(result);
IBManager::close(std::move(result->first));
IBManager::close(std::move(result->second));
}());
}
TEST_F(TestIBSocket, ClientToServer) {
static constexpr int msgs = 100;
auto config = TestUtils::testCfg();
auto result = connectSync(config);
ASSERT_OK(result);
std::jthread server([socket = std::move(result->first)]() {
while (true) {
auto msg = TestMsg::recv(*socket);
if (msg.hasError()) {
ASSERT_ERROR(msg, RPCCode::kSocketClosed);
break;
}
}
});
std::jthread client([socket = std::move(result->second)]() mutable {
for (int i = 0; i < msgs; i++) {
ASSERT_OK(TestMsg::send(*socket, folly::Random::rand32(4, 2 << 20)));
}
IBManager::close(std::move(socket));
});
}
TEST_F(TestIBSocket, ServerToClient) {
static constexpr int msgs = 100;
auto config = TestUtils::testCfg();
auto result = connectSync(config);
ASSERT_OK(result);
std::jthread server([socket = std::move(result->first)]() mutable {
for (int i = 0; i < msgs; i++) {
ASSERT_OK(TestMsg::send(*socket, folly::Random::rand32(4, 2 << 20)));
}
IBManager::close(std::move(socket));
});
std::jthread clntTh([socket = std::move(result->second)]() {
for ([[maybe_unused]] int i = 0; true; i++) {
auto msg = TestMsg::recv(*socket);
if (msg.hasError()) {
ASSERT_ERROR(msg, RPCCode::kSocketClosed);
break;
}
}
});
}
TEST_F(TestIBSocket, BIDirection) {
static constexpr int msgs = 100;
auto config = TestUtils::testCfg();
auto result = connectSync(config);
ASSERT_OK(result);
SCOPE_EXIT {
IBManager::close(std::move(result->first));
IBManager::close(std::move(result->second));
};
std::atomic<uint32_t> cnt(0);
auto send = [&](IBSocket &socket) {
SCOPE_EXIT { cnt++; };
for (int i = 0; i < msgs; i++) {
ASSERT_OK(TestMsg::send(socket, folly::Random::rand32(4, 2 << 20), false));
}
};
auto recv = [&](IBSocket &socket) {
SCOPE_EXIT { cnt++; };
for (int i = 0; i < msgs; i++) {
ASSERT_OK(TestMsg::recv(socket, false));
}
};
auto poll = [&](IBSocket &socket) {
while (cnt < 4) {
auto result = socket.poll(0);
if (result.hasError()) {
ASSERT_ERROR(result, RPCCode::kSocketClosed);
break;
}
}
};
std::jthread server_send([&]() { send(*result->first); });
std::jthread server_recv([&]() { recv(*result->first); });
std::jthread server_poll([&]() { poll(*result->first); });
std::jthread client_send([&]() { send(*result->second); });
std::jthread client_recv([&]() { recv(*result->second); });
std::jthread client_poll([&]() { poll(*result->second); });
}
TEST_F(TestIBSocket, BIDirectionFaultInjection) {
auto config = TestUtils::testCfg();
auto result = connectSync(config);
ASSERT_OK(result);
SCOPE_EXIT {
IBManager::close(std::move(result->first));
IBManager::close(std::move(result->second));
};
std::atomic<uint32_t> cnt(0);
auto inject = [&](IBSocket &socket) {
SCOPE_EXIT { cnt++; };
folly::coro::blockingWait([&]() -> CoTask<void> {
// try to inject a error by post a invalid RDMA operation, shouldn't receive corruption message.
co_await folly::coro::sleep(std::chrono::milliseconds(1000));
// underlying RDMABuf is deregistered immediately, so remote buffer is invalid
auto remote = RDMABuf::allocate(1024).toRemoteBuf();
auto local = RDMABuf::allocate(1024);
auto batch = socket.rdmaWriteBatch();
batch.add(remote, local);
auto result = co_await batch.post();
CO_ASSERT_TRUE(result.hasError());
}());
};
auto send = [&](IBSocket &socket) {
SCOPE_EXIT { cnt++; };
while (true) {
auto result = TestMsg::send(socket, folly::Random::rand32(4, 2 << 20), false);
if (result.hasError()) {
ASSERT_ERROR(result, RPCCode::kSocketError);
break;
}
}
};
auto recv = [&](IBSocket &socket) {
SCOPE_EXIT { cnt++; };
while (true) {
auto result = TestMsg::recv(socket, false);
if (result.hasError()) {
ASSERT_ERROR(result, RPCCode::kSocketError);
break;
}
}
};
auto poll = [&](IBSocket &socket) {
while (cnt < 5) {
auto result = socket.poll(0);
if (result.hasError()) {
bool closed = result.error().code() == RPCCode::kSocketClosed;
bool error = result.error().code() == RPCCode::kSocketError;
ASSERT_TRUE(closed || error) << result.error().describe();
break;
}
}
};
std::jthread inject_th([&]() { inject(*result->first); });
std::jthread server_send([&]() { send(*result->first); });
std::jthread server_recv([&]() { recv(*result->first); });
std::jthread server_poll([&]() { poll(*result->first); });
std::jthread client_send([&]() { send(*result->second); });
std::jthread client_recv([&]() { recv(*result->second); });
std::jthread client_poll([&]() { poll(*result->second); });
}
TEST_F(TestIBSocket, Benchmark) {
size_t benchSize = FLAGS_ib_bench_mb * (1ULL << 20);
auto config = IBSocket::Config();
if (FLAGS_ib_bench_buf_kb) config.set_buf_size(FLAGS_ib_bench_buf_kb * 1024);
if (FLAGS_ib_bench_buf_cnt) config.set_send_buf_cnt(FLAGS_ib_bench_buf_cnt);
auto result = connectSync(config);
ASSERT_OK(result);
UtcTime begin = UtcClock::now();
std::jthread server([benchSize, socket = std::move(result->first)]() {
std::vector<uint8_t> buf(4 << 20, 0);
size_t recved = 0;
while (recved < benchSize) {
size_t rsize = std::min(buf.size(), benchSize - recved);
auto result = TestUtils::recv(*socket, folly::MutableByteRange(buf.data(), rsize));
ASSERT_OK(result);
recved += rsize;
}
});
std::jthread client([benchSize, socket = std::move(result->second)]() {
std::vector<uint8_t> buf(4 << 20, 0);
size_t sended = 0;
while (sended < benchSize) {
size_t wsize = std::min(buf.size(), benchSize - sended);
auto result = TestUtils::send(*socket, folly::ByteRange(buf.data(), wsize));
ASSERT_OK(result);
sended += wsize;
}
});
server.join();
client.join();
auto dur = UtcClock::now() - begin;
std::cout << "IBSocket benchmark bandwidth: "
<< ((double)FLAGS_ib_bench_mb / std::chrono::duration_cast<std::chrono::milliseconds>(dur).count() * 1000.0)
<< " MB/s" << std::endl;
}
TEST_F(TestIBSocket, PingPong) {
size_t kLoops = FLAGS_ib_pingpong_loops;
size_t kMsgSize = FLAGS_ib_pingpong_size;
auto config = IBSocket::Config();
auto result = connectSync(config);
ASSERT_OK(result);
UtcTime begin = UtcClock::now();
std::jthread server([kMsgSize, kLoops, socket = std::move(result->first)]() {
std::vector<uint8_t> buf(kMsgSize, 0);
for (size_t i = 0; i < kLoops; i++) {
ASSERT_OK(TestUtils::recv(*socket, folly::MutableByteRange(buf.data(), kMsgSize)));
ASSERT_OK(TestUtils::send(*socket, folly::ByteRange(buf.data(), kMsgSize)));
}
});
std::jthread client([kMsgSize, kLoops, socket = std::move(result->second)]() {
std::vector<uint8_t> buf(kMsgSize, 0);
for (size_t i = 0; i < kLoops; i++) {
ASSERT_OK(TestUtils::send(*socket, folly::ByteRange(buf.data(), kMsgSize)));
ASSERT_OK(TestUtils::recv(*socket, folly::MutableByteRange(buf.data(), kMsgSize)));
}
});
server.join();
client.join();
auto dur = UtcClock::now() - begin;
std::cout << "IBSocket pingpong latency: "
<< std::chrono::duration_cast<std::chrono::nanoseconds>(dur).count() / 1000.0 / kLoops << "us" << std::endl;
}
TEST_F(TestIBSocket, RDMAWrite) {
auto config = TestUtils::testCfg();
auto result = connectSync(config);
ASSERT_OK(result);
SCOPE_EXIT {
IBManager::close(std::move(result->first));
IBManager::close(std::move(result->second));
};
std::atomic<bool> stop = false;
auto jthreads = TestUtils::poll({result->first.get(), result->second.get()}, stop);
folly::coro::blockingWait([&, &socket = *result->first]() -> CoTask<void> {
auto pool = RDMABufPool::create(4 << 10, 10);
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
// RDMABufPool
auto src = co_await pool->allocate();
auto dst = co_await pool->allocate();
CO_ASSERT_TRUE(src);
CO_ASSERT_TRUE(dst);
for (size_t i = 0; i < src.size(); i++) {
((uint8_t *)src.ptr())[i] = folly::Random::rand32();
}
auto crc32c = folly::crc32c((uint8_t *)src.ptr(), src.size());
auto dstRemote = dst.toRemoteBuf();
CO_ASSERT_TRUE(dstRemote);
auto result = co_await socket.rdmaWrite(dstRemote, src);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(folly::crc32c((uint8_t *)dst.ptr(), dst.size()), crc32c);
}
auto remotePool = RDMABufPool::create(4096 * 16, -1);
auto localPool = RDMABufPool::create(4096, -1);
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
std::vector<RDMABuf> remotes;
std::vector<RDMABuf> locals;
auto batch = socket.rdmaWriteBatch();
int numReqs = folly::Random::rand32(1, 64);
for (int i = 0; i < numReqs; i++) {
size_t total = 0;
int numLocalBufs = folly::Random::rand32(1, 16);
for (int j = 0; j < numLocalBufs; j++) {
auto size = folly::Random::rand32(4, 4096);
auto local = co_await localPool->allocate();
local = local.first(size);
CO_ASSERT_TRUE(local);
locals.push_back(local);
total += size;
}
auto remote = co_await remotePool->allocate();
remote = remote.first(total);
CO_ASSERT_TRUE(remote);
remotes.push_back(remote);
batch.add(remote.toRemoteBuf(), std::span(locals).last(numLocalBufs));
}
auto crc32c = 0;
for (auto buf : locals) {
for (size_t i = 0; i < buf.size(); i++) {
((uint8_t *)buf.ptr())[i] = folly::Random::rand32();
}
crc32c = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32c);
}
auto result = co_await batch.post();
CO_ASSERT_OK(result);
auto crc32cOut = 0;
for (auto buf : remotes) {
crc32cOut = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32cOut);
}
CO_ASSERT_EQ(crc32c, crc32cOut);
}
stop = true;
}());
}
TEST_F(TestIBSocket, RDMARead) {
auto config = TestUtils::testCfg();
auto result = connectSync(config);
ASSERT_OK(result);
SCOPE_EXIT {
IBManager::close(std::move(result->first));
IBManager::close(std::move(result->second));
};
std::atomic<bool> stop = false;
auto jthreads = TestUtils::poll({result->first.get(), result->second.get()}, stop);
folly::coro::blockingWait([&, &socket = *result->first]() -> CoTask<void> {
auto pool = RDMABufPool::create(4 << 10, 10);
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
// RDMABufPool
auto src = co_await pool->allocate();
auto dst = co_await pool->allocate();
CO_ASSERT_TRUE(src);
CO_ASSERT_TRUE(dst);
for (size_t i = 0; i < src.size(); i++) {
((uint8_t *)src.ptr())[i] = folly::Random::rand32();
}
auto crc32c = folly::crc32c((uint8_t *)src.ptr(), src.size());
auto srcRemote = src.toRemoteBuf();
CO_ASSERT_TRUE(srcRemote);
auto result = co_await socket.rdmaRead(srcRemote, dst);
CO_ASSERT_OK(result);
CO_ASSERT_EQ(folly::crc32c((uint8_t *)dst.ptr(), dst.size()), crc32c);
}
auto remotePool = RDMABufPool::create(4096 * 16, -1);
auto localPool = RDMABufPool::create(4096, -1);
for (size_t i = 0; i < FLAGS_ib_rdma_iters; i++) {
XLOGF_IF(INFO, i % 100 == 0, "Run {}", i);
std::vector<RDMABuf> remotes;
std::vector<RDMABuf> locals;
auto batch = socket.rdmaReadBatch();
int numReqs = folly::Random::rand32(1, 64);
for (int i = 0; i < numReqs; i++) {
size_t total = 0;
int numLocalBufs = folly::Random::rand32(1, 16);
for (int j = 0; j < numLocalBufs; j++) {
auto size = folly::Random::rand32(4, 4 << 10);
auto local = co_await localPool->allocate();
local = local.first(size);
CO_ASSERT_TRUE(local);
locals.push_back(local);
total += size;
}
auto remote = co_await remotePool->allocate();
remote = remote.first(total);
CO_ASSERT_TRUE(remote);
remotes.push_back(remote);
batch.add(remote.toRemoteBuf(), std::span(locals).last(numLocalBufs));
}
auto crc32c = 0;
for (auto buf : remotes) {
for (size_t i = 0; i < buf.size(); i++) {
((uint8_t *)buf.ptr())[i] = folly::Random::rand32();
}
crc32c = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32c);
}
auto result = co_await batch.post();
CO_ASSERT_OK(result);
auto crc32cOut = 0;
for (auto buf : locals) {
crc32cOut = folly::crc32c((uint8_t *)buf.ptr(), buf.size(), crc32cOut);
}
CO_ASSERT_EQ(crc32c, crc32cOut);
}
stop = true;
}());
}
TEST_F(TestIBSocket, RemoteClosed) {
auto config = IBSocket::Config();
auto result = connectSync(config);
ASSERT_OK(result);
boost::barrier barrier(2);
std::jthread server([socket = std::move(result->first)]() mutable {
ASSERT_OK(TestMsg::send(*socket, 4 << 20));
IBManager::close(std::move(socket));
});
std::jthread client([socket = std::move(result->second)]() mutable {
ASSERT_OK(TestMsg::recv(*socket));
ASSERT_ERROR(TestMsg::recv(*socket), RPCCode::kSocketClosed);
ASSERT_ERROR(TestMsg::send(*socket, 1 << 20), RPCCode::kSocketClosed);
ASSERT_ERROR(TestMsg::send(*socket, 1 << 20), RPCCode::kSocketClosed);
IBManager::close(std::move(socket));
});
}
TEST_F(TestIBSocket, RDMAFailure) {
auto config = TestUtils::testCfg();
auto result = connectSync(config);
ASSERT_OK(result);
SCOPE_EXIT {
IBManager::close(std::move(result->first));
IBManager::close(std::move(result->second));
};
// connection is OK
{
auto &client = *result->first;
auto deadline = RelativeTime::now() + 5_s;
while (RelativeTime::now() < deadline) {
ASSERT_OK(client.check());
auto result = client.poll(0);
ASSERT_OK(result);
}
}
// post invalid RDMA request on server side to make
std::atomic<bool> stop = false;
auto jthreads = TestUtils::poll({result->second.get()}, stop);
folly::coro::blockingWait([&, &server = *result->second]() -> CoTask<void> {
// post RDMA with invalid rkey
// underlying RDMABuf is deregistered immediately, so remote buffer is invalid
auto remote = RDMABuf::allocate(1024).toRemoteBuf();
auto local = RDMABuf::allocate(1024);
for (int i = 0; i < 2; i++) {
auto batch = server.rdmaWriteBatch();
batch.add(remote, local);
batch.add(remote, local);
batch.add(remote, local);
batch.add(remote, local);
auto result = co_await batch.post();
CO_ASSERT_TRUE(result.hasError());
std::cout << result.error().describe() << std::endl;
}
stop = true;
}());
// connection is broken, send check message on client side
auto &client = *result->first;
auto deadline = RelativeTime::now() + 10_s;
auto error = false;
while (RelativeTime::now() < deadline) {
ASSERT_OK(client.check());
auto result = client.poll(0);
if (result.hasError()) {
error = true;
fmt::print("{}\n", result.error());
break;
} else {
fmt::print(".");
}
std::this_thread::sleep_for(1_s);
}
ASSERT_TRUE(error) << "client side doesn't get error after send check message";
}
TEST_F(TestIBSocket, CloseBeforeConnect) {
auto config = TestUtils::testCfg();
auto addr = serverAddr();
Address tcpAddr(addr.ip, addr.port, Address::TCP);
auto ctx = client_.serdeCtx(tcpAddr);
auto socket = std::make_unique<IBSocket>(config);
folly::coro::blockingWait(socket->close());
ASSERT_ERROR(folly::coro::blockingWait(socket->connect(ctx, 1_s)), RPCCode::kSocketClosed);
}
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,405 @@
#include <algorithm>
#include <chrono>
#include <folly/Executor.h>
#include <folly/Random.h>
#include <folly/ScopeGuard.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/executors/IOThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/futures/Future.h>
#include <folly/logging/LogConfig.h>
#include <folly/logging/LogConfigParser.h>
#include <folly/logging/Logger.h>
#include <folly/logging/LoggerDB.h>
#include <folly/logging/ObjectToString.h>
#include <folly/logging/xlog.h>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include <memory>
#include <random>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
#include "common/net/Client.h"
#include "common/net/Listener.h"
#include "common/net/Server.h"
#include "common/net/ib/IBConnectService.h"
#include "common/net/ib/IBDevice.h"
#include "common/net/ib/RDMABuf.h"
#include "common/net/sync/Client.h"
#include "common/serde/ClientContext.h"
#include "common/serde/Serde.h"
#include "common/serde/Service.h"
#include "common/utils/Address.h"
#include "common/utils/Coroutine.h"
#include "common/utils/Result.h"
#include "common/utils/UtcTime.h"
#include "tests/GtestHelpers.h"
#include "tests/common/net/ib/SetupIB.h"
DEFINE_bool(test_rdma_disable_log, false, "disable in TestRDMA");
DEFINE_double(test_rdma_fault_injection_rate, 0.001, "fault injection rate in TestRDMA");
DEFINE_int32(test_rdma_connections, 4, "connections in TestRDMA");
DEFINE_int32(test_rdma_pkey_index, 0, "pkey_index in TestRDMA");
namespace hf3fs::net {
struct RDMAReq {
SERDE_STRUCT_FIELD(bufs, std::vector<RDMARemoteBuf>{});
SERDE_STRUCT_FIELD(val, uint64_t(0));
};
struct RDMARsp {
SERDE_STRUCT_FIELD(dummy, Void());
};
SERDE_SERVICE(RDMAService, 87) { SERDE_SERVICE_METHOD(test, 1, RDMAReq, RDMARsp); };
class RDMAServiceImpl : public serde::ServiceWrapper<RDMAServiceImpl, RDMAService> {
public:
RDMAServiceImpl()
: bufPool_() {
bufPool_ = net::RDMABufPool::create(1 << 20, 1024);
}
CoTryTask<RDMARsp> test(serde::CallContext &ctx, const RDMAReq &req) {
RDMARsp rsp;
auto *ibsocket = ctx.transport()->ibSocket();
XLOGF_IF(FATAL, ibsocket == nullptr, "Not IBSocket");
co_await folly::coro::sleep(std::chrono::milliseconds(folly::Random::rand32(100)));
if (!req.bufs.empty()) {
auto buf = co_await bufPool_->allocate();
if (!buf) {
co_return makeError(RPCCode::kRDMANoBuf);
}
auto batch = ibsocket->rdmaWriteBatch();
for (auto remote : req.bufs) {
if (buf.size() < remote.size()) {
auto result = co_await batch.post();
if (result.hasError()) {
co_return makeError(std::move(result.error()));
}
batch.clear();
buf.resetRange();
}
auto subbuf = buf.takeFirst(remote.size());
if (!subbuf) {
EXPECT_TRUE(false);
co_return makeError(StatusCode::kInvalidArg);
}
XLOGF_IF(FATAL, req.val == 0, "here");
*(uint64_t *)subbuf.ptr() = req.val;
if (subbuf.size() >= 16) {
*(uint64_t *)(subbuf.ptr() + subbuf.size() - 8) = req.val;
}
batch.add(remote, subbuf);
}
auto result = co_await batch.post();
if (result.hasError()) {
co_return makeError(std::move(result.error()));
}
}
co_return rsp;
}
private:
std::shared_ptr<net::RDMABufPool> bufPool_;
};
using namespace std::chrono_literals;
class TestRDMA : public test::SetupIB {
public:
void SetUp() override {
auto &logger = folly::LoggerDB::get();
if (FLAGS_test_rdma_disable_log) {
oldLogConfig_ = logger.getConfig();
logger.updateConfig(folly::parseLogConfig("CRITICAL"));
}
server_ = std::make_unique<Server>(serverConfig_);
server_->addSerdeService(std::make_unique<RDMAServiceImpl>());
ASSERT_TRUE(server_->setup());
ASSERT_TRUE(server_->start());
ASSERT_TRUE(client_.start());
clientConfig_.io_worker().ibsocket().set_pkey_index(FLAGS_test_rdma_pkey_index);
}
void TearDown() override {
if (FLAGS_test_rdma_disable_log) {
folly::LoggerDB::get().updateConfig(oldLogConfig_);
}
client_.stopAndJoin();
if (server_) {
server_->stopAndJoin();
server_.reset();
}
}
Address serverAddr() const { return server_->groups().front()->addressList().front(); }
void serverRestart() {
if (server_) {
serverConfig_.groups(0).listener().set_listen_port(serverAddr().port);
server_->stopAndJoin();
corpse_.push_back(std::move(server_));
}
server_ = std::make_unique<Server>(serverConfig_);
server_->addSerdeService(std::make_unique<RDMAServiceImpl>());
ASSERT_TRUE(server_->setup());
ASSERT_TRUE(server_->start());
}
void serverDropConnections() {
for (auto &grp : server_->groups()) {
grp->ioWorker().dropConnections(true, true);
}
}
protected:
Client::Config clientConfig_;
bool initClientConfig = [this] {
clientConfig_.io_worker().transport_pool().set_max_connections(FLAGS_test_rdma_connections);
clientConfig_.io_worker().ibsocket().set_max_rdma_wr(16);
clientConfig_.io_worker().ibsocket().set_max_rdma_wr_per_post(4);
clientConfig_.io_worker().set_num_event_loop(2);
clientConfig_.thread_pool().set_num_connect_threads(2);
clientConfig_.thread_pool().set_num_io_threads(4);
clientConfig_.thread_pool().set_num_proc_threads(4);
clientConfig_.set_default_timeout(2_s);
return true;
}();
Client client_{clientConfig_};
Server::Config serverConfig_;
bool initServerConfig = [this] {
serverConfig_.groups(0).set_network_type(Address::RDMA);
serverConfig_.thread_pool().set_num_io_threads(4);
serverConfig_.thread_pool().set_num_proc_threads(4);
return true;
}();
std::unique_ptr<Server> server_;
std::vector<std::unique_ptr<Server>> corpse_;
folly::LogConfig oldLogConfig_;
};
CoTask<void> runClient(RDMAService<> client,
serde::ClientContext ctx,
size_t bufSize,
size_t bufCnt,
bool injectFault,
bool allowError,
Duration dur,
net::UserRequestOptions opts = {}) {
auto invalidMR = net::RDMABuf::allocate(bufSize).toRemoteBuf();
auto buf = net::RDMABuf::allocate(bufSize * bufCnt);
CO_ASSERT_TRUE(buf);
std::vector<net::RDMABuf> rdmaBufs;
for (size_t i = 0; i < bufCnt; i++) {
auto subBuf = buf.takeFirst(bufSize);
CO_ASSERT_TRUE(subBuf);
rdmaBufs.push_back(subBuf);
}
auto begin = SteadyClock::now();
do {
// call RPCs here
auto val = folly::Random::rand64();
RDMAReq req;
req.val = val;
XLOGF(DBG, "request {}", req.val);
// use random number of buf
size_t numBuf = folly::Random::rand32(rdmaBufs.size());
for (size_t bufIdx = 0; bufIdx < numBuf; bufIdx++) {
// buf with random length
auto buf = rdmaBufs.at(bufIdx);
auto len = folly::Random::rand32(8, buf.size() + 1);
auto subbuf = buf.first(len);
CO_ASSERT_TRUE(subbuf);
*(uint64_t *)subbuf.ptr() = 0;
if (subbuf.size() >= 16) {
*(uint64_t *)(subbuf.ptr() + subbuf.size() - 8) = 0;
}
auto mr = subbuf.toRemoteBuf();
if (injectFault && folly::Random::randDouble01() < FLAGS_test_rdma_fault_injection_rate) {
mr = invalidMR.first(mr.size());
}
req.bufs.push_back(mr);
}
std::shuffle(req.bufs.begin(), req.bufs.end(), std::mt19937());
auto result = co_await client.test(ctx, req, &opts);
if (!allowError) {
CO_ASSERT_OK(result);
}
if (!result.hasError()) {
for (auto subbuf : req.bufs) {
// check data
CO_ASSERT_EQ(*(uint64_t *)subbuf.addr(), val);
if (subbuf.size() >= 16) {
CO_ASSERT_EQ(*(uint64_t *)(subbuf.addr() + subbuf.size() - 8), val);
}
}
} else {
XLOGF(DBG, "req {}, error {}", req.val, result.error());
}
} while (SteadyClock::now() - begin <= dur);
}
CoTask<void> startMultiClient(folly::Executor::KeepAlive<> exec,
size_t nClients,
RDMAService<> client,
serde::ClientContext ctx,
size_t bufSize,
size_t bufCnt,
bool injectFault,
bool allowError,
Duration dur,
net::UserRequestOptions opts = {}) {
std::vector<folly::SemiFuture<Void>> tasks;
for (size_t i = 0; i < nClients; i++) {
tasks.push_back(
runClient(client, ctx, bufSize, bufCnt, injectFault, allowError, dur, opts).scheduleOn(exec).start());
}
co_await folly::coro::collectAllRange(std::move(tasks));
}
TEST_F(TestRDMA, Basic) {
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
co_await runClient(client, ctx, 512 << 10, 8, false, false, 5_s);
}));
}
TEST_F(TestRDMA, Concurrent) {
auto ctx = client_.serdeCtx(serverAddr());
folly::CPUThreadPoolExecutor exec(8);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, false, 10_s);
}));
}
TEST_F(TestRDMA, ShortTimeout) {
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
net::UserRequestOptions opt;
opt.timeout = 100_ms;
co_await runClient(client, ctx, 512 << 10, 8, false, true, 5_s, opt);
}));
}
TEST_F(TestRDMA, ShortTimeoutConcurrent) {
auto ctx = client_.serdeCtx(serverAddr());
folly::CPUThreadPoolExecutor exec(8);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
net::UserRequestOptions opt;
opt.timeout = 100_ms;
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, true, 10_s, opt);
}));
}
TEST_F(TestRDMA, FaultInject) {
auto ctx = client_.serdeCtx(serverAddr());
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
co_await runClient(client, ctx, 512 << 10, 8, true, true, 5_s);
co_await runClient(client, ctx, 512 << 10, 8, false, true, 5_s);
co_await runClient(client, ctx, 512 << 10, 8, false, false, 5_s);
}));
}
TEST_F(TestRDMA, FaultInjectConcurrent) {
auto ctx = client_.serdeCtx(serverAddr());
folly::CPUThreadPoolExecutor exec(8);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
co_await startMultiClient(&exec, 32, client, ctx, 512 << 10, 8, true, true, 5_s);
co_await startMultiClient(&exec, 32, client, ctx, 512 << 10, 8, false, true, 5_s);
co_await startMultiClient(&exec, 32, client, ctx, 512 << 10, 8, false, false, 5_s);
}));
}
TEST_F(TestRDMA, SlowConnect) {
IBConnectService::delayMs() = 200;
SCOPE_EXIT { IBConnectService::delayMs() = 0; };
auto ctx = client_.serdeCtx(serverAddr());
folly::CPUThreadPoolExecutor exec(8);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
net::UserRequestOptions opt;
opt.timeout = 200_ms;
co_await runClient(client, ctx, 512 << 10, 8, false, true, 5_s, opt);
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, true, 10_s, opt);
}));
}
TEST_F(TestRDMA, ConnectitonLost) {
IBConnectService::connectionLost() = true;
SCOPE_EXIT { IBConnectService::connectionLost() = false; };
auto ctx = client_.serdeCtx(serverAddr());
folly::CPUThreadPoolExecutor exec(8);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
net::UserRequestOptions opt;
co_await runClient(client, ctx, 512 << 10, 8, false, true, 100_ms, opt);
co_await folly::coro::sleep(std::chrono::seconds(20));
}));
}
TEST_F(TestRDMA, ServerDropConnections) {
auto opts = net::UserRequestOptions{};
opts.sendRetryTimes = 0;
auto ctx = client_.serdeCtx(serverAddr());
folly::CPUThreadPoolExecutor exec(8);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
for (size_t i = 0; i < 3; i++) {
serverDropConnections();
co_await folly::coro::sleep(std::chrono::milliseconds(200));
co_await runClient(client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
serverDropConnections();
co_await folly::coro::sleep(std::chrono::milliseconds(200));
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
}
}));
}
TEST_F(TestRDMA, ServerRestart) {
auto opts = net::UserRequestOptions{};
opts.sendRetryTimes = 0;
auto ctx = client_.serdeCtx(serverAddr());
folly::CPUThreadPoolExecutor exec(8);
folly::coro::blockingWait(folly::coro::co_invoke([&]() -> CoTask<void> {
RDMAService<> client;
for (size_t i = 0; i < 5; i++) {
serverRestart();
co_await runClient(client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
serverRestart();
co_await startMultiClient(&exec, 128, client, ctx, 512 << 10, 8, false, false, 500_ms, opts);
}
}));
}
} // namespace hf3fs::net

View File

@@ -0,0 +1,230 @@
#include <atomic>
#include <chrono>
#include <folly/Random.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <gtest/gtest.h>
#include <map>
#include <thread>
#include <vector>
#include "SetupIB.h"
#include "common/net/ib/IBDevice.h"
#include "common/net/ib/RDMABuf.h"
#include "common/utils/Coroutine.h"
#include "gtest/gtest.h"
namespace hf3fs::net {
class TestRDMARemoteBuf : public test::SetupIB {};
TEST_F(TestRDMARemoteBuf, Basic) {
RDMARemoteBuf buf;
ASSERT_FALSE(buf);
for (auto rkey : buf.rkeys_) {
EXPECT_EQ(rkey, RDMARemoteBuf::Rkey());
}
auto rdmaBuf = RDMABuf::allocate(4 << 20);
ASSERT_TRUE(rdmaBuf);
buf = rdmaBuf.toRemoteBuf();
ASSERT_TRUE(buf);
}
TEST_F(TestRDMARemoteBuf, Subrange) {
auto rdmaBuf = RDMABuf::allocate(4 << 20);
ASSERT_TRUE(rdmaBuf);
const auto buf = rdmaBuf.toRemoteBuf();
ASSERT_TRUE(buf);
ASSERT_EQ(buf.size(), 4 << 20);
auto buf1 = buf;
ASSERT_EQ(buf1, buf);
ASSERT_EQ(buf1.advance(1 << 20), true);
ASSERT_EQ(buf1.size(), 3 << 20);
ASSERT_EQ(buf1.subtract(1 << 20), true);
ASSERT_EQ(buf1.size(), 2 << 20);
ASSERT_FALSE(buf1.advance(buf1.size() + 1));
ASSERT_FALSE(buf1.subtract(buf1.size() + 1));
ASSERT_NE(buf, buf1);
ASSERT_EQ(buf.subrange(1 << 20, 2 << 20), buf1);
ASSERT_EQ(buf.subrange(2 << 20, 1 << 20), buf1.subrange(1 << 20, 1 << 20));
auto buf2 = buf;
auto buf3 = buf2.first(1 << 20);
auto buf4 = buf2.takeFirst(1 << 20);
ASSERT_EQ(buf3, buf4);
ASSERT_EQ(buf2, buf.subrange(1 << 20, 3 << 20));
ASSERT_EQ(buf3.addr(), buf.addr());
ASSERT_EQ(buf2.size(), 3 << 20);
ASSERT_EQ(buf2.addr(), buf.addr() + (1 << 20));
auto buf5 = buf;
auto buf6 = buf.last(1 << 20);
auto buf7 = buf5.takeLast(1 << 20);
ASSERT_EQ(buf6, buf7);
ASSERT_EQ(buf6, buf.subrange(3 << 20, 1 << 20));
ASSERT_EQ(buf5.size(), 3 << 20);
ASSERT_EQ(buf5.addr(), buf.addr());
ASSERT_TRUE(buf.subrange(1 << 20, 3 << 20));
ASSERT_FALSE(buf.subrange(4 << 20, 1));
ASSERT_TRUE(buf.subrange(4 << 20, 0));
ASSERT_TRUE(buf.first(buf.size()));
ASSERT_TRUE(buf.last(buf.size()));
ASSERT_FALSE(buf.first(buf.size() + 1));
ASSERT_FALSE(buf.last(buf.size() + 1));
auto buf8 = buf;
ASSERT_FALSE(buf8.advance(buf.size() + 1));
ASSERT_FALSE(buf8.subtract(buf.size() + 1));
}
class TestRDMABuf : public test::SetupIB {};
TEST_F(TestRDMABuf, Default) {
RDMABuf buf;
ASSERT_FALSE(buf);
ASSERT_FALSE(buf.toRemoteBuf());
ASSERT_FALSE(buf.toRemoteBuf().getRkey(0));
RDMABuf buf2 = buf;
ASSERT_FALSE(buf2);
}
TEST_F(TestRDMABuf, Allocate) {
std::queue<RDMABuf> bufs;
for (auto i = 0; i < 100; i++) {
auto size = folly::Random::rand32(8, 8 << 20);
auto buf = RDMABuf::allocate(size);
ASSERT_TRUE(buf);
ASSERT_NE(buf.ptr(), nullptr);
ASSERT_EQ(buf.size(), size);
bufs.push(buf);
}
while (!bufs.empty()) {
auto buf = std::move(bufs.front());
bufs.pop();
std::map<int, RDMABufMR> map;
for (auto &dev : IBDevice::all()) {
auto mr = buf.getMR(dev->id());
map[dev->id()] = mr;
ASSERT_NE(mr, nullptr);
}
ASSERT_TRUE(buf.toRemoteBuf());
// ASSERT_EQ(buf.reregMR(), 0);
// for (auto &dev : IBDevice::all()) {
// auto mr = buf.getMR(dev->id());
// ASSERT_NE(mr, nullptr);
// ASSERT_NE(mr, map[dev->id()]);
// }
}
}
TEST_F(TestRDMABuf, Subrange) {
const auto buf = RDMABuf::allocate(4 << 20);
ASSERT_TRUE(buf);
ASSERT_EQ(buf.size(), 4 << 20);
ASSERT_EQ(buf.capacity(), buf.size());
auto buf1 = buf;
ASSERT_EQ(buf1, buf);
ASSERT_EQ(buf1.advance(1 << 20), true);
ASSERT_EQ(buf1.capacity(), 4 << 20);
ASSERT_EQ(buf1.size(), 3 << 20);
ASSERT_EQ(buf1.subtract(1 << 20), true);
ASSERT_EQ(buf1.capacity(), 4 << 20);
ASSERT_EQ(buf1.size(), 2 << 20);
ASSERT_FALSE(buf1.advance(buf1.size() + 1));
ASSERT_FALSE(buf1.subtract(buf1.size() + 1));
ASSERT_NE(buf, buf1);
ASSERT_EQ(buf.subrange(1 << 20, 2 << 20), buf1);
ASSERT_EQ(buf.subrange(2 << 20, 1 << 20), buf1.subrange(1 << 20, 1 << 20));
auto buf2 = buf;
auto buf3 = buf2.first(1 << 20);
auto buf4 = buf2.takeFirst(1 << 20);
ASSERT_EQ(buf3, buf4);
ASSERT_EQ(buf2, buf.subrange(1 << 20, 3 << 20));
ASSERT_EQ(buf3.ptr(), buf.ptr());
ASSERT_EQ(buf2.size(), 3 << 20);
ASSERT_EQ(buf2.ptr(), buf.ptr() + (1 << 20));
auto buf5 = buf;
auto buf6 = buf.last(1 << 20);
auto buf7 = buf5.takeLast(1 << 20);
ASSERT_EQ(buf6, buf7);
ASSERT_EQ(buf6, buf.subrange(3 << 20, 1 << 20));
ASSERT_EQ(buf5.size(), 3 << 20);
ASSERT_EQ(buf5.ptr(), buf.ptr());
ASSERT_TRUE(buf.subrange(1 << 20, 3 << 20));
ASSERT_FALSE(buf.subrange(4 << 20, 1));
ASSERT_TRUE(buf.subrange(4 << 20, 0));
ASSERT_TRUE(buf.first(buf.size()));
ASSERT_TRUE(buf.last(buf.size()));
ASSERT_FALSE(buf.first(buf.size() + 1));
ASSERT_FALSE(buf.last(buf.size() + 1));
auto buf8 = buf;
ASSERT_FALSE(buf8.advance(buf.size() + 1));
ASSERT_FALSE(buf8.subtract(buf.size() + 1));
}
class TestRDMABufPool : public test::SetupIB {};
TEST_F(TestRDMABufPool, Basic) {
constexpr size_t kBufSize = 4 << 10L;
constexpr size_t kBufNum = 10;
auto pool = RDMABufPool::create(kBufSize, kBufNum);
folly::coro::blockingWait([&]() -> CoTask<void> {
std::queue<RDMABuf> bufs;
for (size_t i = 0; i < kBufNum; i++) {
auto buf = co_await pool->allocate();
CO_ASSERT_TRUE(buf);
bufs.push(buf);
}
auto buf = co_await pool->allocate(std::chrono::milliseconds(10));
CO_ASSERT_FALSE(buf);
auto firstPtr = bufs.front().ptr();
std::atomic_bool allocated(false);
auto firstBuf = bufs.front();
bufs.pop();
std::jthread proc([&allocated, first = std::move(firstBuf)]() {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
EXPECT_EQ(allocated.load(), false);
});
buf = co_await pool->allocate();
CO_ASSERT_EQ(firstPtr, buf.ptr());
bufs.push(buf);
while (!bufs.empty()) {
auto buf = bufs.front();
bufs.pop();
CO_ASSERT_EQ(buf.capacity(), kBufSize);
CO_ASSERT_NE(buf.ptr(), nullptr);
std::map<int, RDMABufMR> map;
for (auto &dev : IBDevice::all()) {
auto mr = buf.getMR(dev->id());
map[dev->id()] = mr;
CO_ASSERT_NE(mr, nullptr);
}
buf.toRemoteBuf();
// CO_ASSERT_EQ(buf.reregMR(), 0);
// for (auto &dev : IBDevice::all()) {
// auto mr = buf.getMR(dev->id());
// CO_ASSERT_NE(mr, nullptr);
// CO_ASSERT_NE(mr, map[dev->id()]);
// }
}
}());
}
} // namespace hf3fs::net

View File

@@ -0,0 +1,24 @@
#include "common/serde/BigEndian.h"
#include "common/serde/Serde.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::serde::test {
namespace {
TEST(TestSerde, BigEndian) {
{
using BigSize = BigEndian<std::size_t>;
BigSize size{0x123456789ABCDEF0ul};
auto out = serde::serialize(size);
ASSERT_EQ(out.size(), sizeof(std::size_t));
ASSERT_EQ(out.front(), 0x12);
ASSERT_EQ(out.back(), (char)0xF0);
ASSERT_EQ(serde::toJsonString(size), std::to_string(size));
ASSERT_OK(serde::fromJsonString(size, "233"));
ASSERT_EQ(size, 233);
}
}
} // namespace
} // namespace hf3fs::serde::test

View File

@@ -0,0 +1,34 @@
#include "common/serde/MessagePacket.h"
#include "common/serde/Serde.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::serde::test {
namespace {
struct Demo {
bool operator==(const Demo &) const = default;
SERDE_STRUCT_FIELD(foo, int{}, nullptr);
SERDE_STRUCT_FIELD(bar, std::string{}, nullptr);
};
TEST(TestMessagePacket, Normal) {
// send.
Demo req;
req.foo = 100;
req.bar = "Good";
MessagePacket send(req);
auto bytes = serde::serialize(send);
XLOGF(INFO, "send json: {}, binary: {:02X}", send, fmt::join(bytes, "-"));
// recv.
MessagePacket recv;
ASSERT_OK(serde::deserialize(recv, bytes));
Demo rsp;
ASSERT_OK(serde::deserialize(rsp, recv.payload));
ASSERT_EQ(req, rsp);
XLOGF(INFO, "rsp json: {}", rsp);
}
} // namespace
} // namespace hf3fs::serde::test

View File

@@ -0,0 +1,946 @@
#include <folly/json.h>
#include "common/serde/Serde.h"
#include "common/utils/DownwardBytes.h"
#include "common/utils/Reflection.h"
#include "common/utils/Result.h"
#include "common/utils/RobinHood.h"
#include "common/utils/StrongType.h"
#include "fbs/mgmtd/RoutingInfo.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
bool checkValue(float b) { return b > 10.0; }
struct CheckString {
constexpr CheckString() = default;
constexpr bool operator()(const std::string &str) const { return !str.empty(); }
};
struct Demo {
SERDE_CLASS_FIELD(a, short{10}, [](short a) { return a > 0; }); // checker is lambda expr.
SERDE_CLASS_FIELD(b, 20.0f, checkValue); // checker is function pointer.
SERDE_CLASS_FIELD(c, std::string{"ok"}, CheckString{}); // checker is function object.
};
static_assert(serde::count<Demo>() == 3);
static_assert(serde::name<Demo, 0>() == "a");
static_assert(serde::name<Demo, 1>() == "b");
static_assert(serde::name<Demo, 2>() == "c");
static_assert(serde::SerdeType<Demo>);
static_assert(!serde::SerdeType<int>);
TEST(TestSerde, Normal) {
const Demo demo;
ASSERT_EQ(serde::name<0>(demo), "a");
ASSERT_EQ(serde::name<1>(demo), "b");
ASSERT_EQ(serde::name<2>(demo), "c");
ASSERT_EQ(serde::value<0>(demo), 10);
ASSERT_EQ(serde::value<1>(demo), 20.0);
ASSERT_EQ(serde::value<2>(demo), "ok");
ASSERT_EQ(&serde::value<0>(demo), &demo.a());
ASSERT_EQ(&serde::value<1>(demo), &demo.b());
ASSERT_EQ(&serde::value<2>(demo), &demo.c());
}
TEST(TestSerde, Serialize) {
Demo demo;
demo.a() = 100;
demo.b() = 200.f;
demo.c() = "hello";
const Demo &d = demo;
auto out = serde::serialize(d);
ASSERT_EQ(out.length(), serde::serializeLength(d));
constexpr auto size = 1 + 2 + 4 + 1 + 5; // struct size + short + float + str length + str content.
ASSERT_EQ(out.length(), size);
XLOGF(INFO, "out is {:02X}", fmt::join(out, ","));
Demo des;
ASSERT_EQ(des.a(), 10);
ASSERT_EQ(des.b(), 20.f);
ASSERT_EQ(des.c(), "ok");
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(des.a(), 100);
ASSERT_EQ(des.b(), 200.f);
ASSERT_EQ(des.c(), "hello");
}
struct Nested {
SERDE_CLASS_FIELD(a, Demo{});
SERDE_CLASS_FIELD(b, Demo{});
};
TEST(TestSerde, Serialize2) {
Nested nested;
nested.b().a() = 100;
nested.b().b() = 200.f;
nested.b().c() = "hello";
auto out = serde::serialize(nested);
ASSERT_EQ(out.length(), serde::serializeLength(nested));
constexpr auto size = 1 + (1 + 2 + 4 + 1 + 2) + (1 + 2 + 4 + 1 + 5);
ASSERT_EQ(out.length(), size);
Nested des;
ASSERT_EQ(des.a().a(), des.b().a());
ASSERT_EQ(des.a().b(), des.b().b());
ASSERT_EQ(des.a().c(), des.b().c());
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(des.a().a(), 10);
ASSERT_EQ(des.a().b(), 20.f);
ASSERT_EQ(des.a().c(), "ok");
ASSERT_EQ(des.b().a(), 100);
ASSERT_EQ(des.b().b(), 200.f);
ASSERT_EQ(des.b().c(), "hello");
}
TEST(TestSerde, Vector) {
struct Test {
SERDE_CLASS_FIELD(a, (std::vector<int>{0, 1, 2}));
SERDE_CLASS_FIELD(b, (std::vector<std::string>{"x", "y"}));
};
Test a;
auto out = serde::serialize(a);
ASSERT_EQ(out.length(), serde::serializeLength(a));
ASSERT_EQ(out.size(), 1 + 1 + 4 * 3 + 1 + 2 * (1 + 1)); // struct size + vsize + int * 3 + vsize + 2 * (ssize + c)
a.a() = std::vector<int>{0, 1, 0, 1};
a.b() = std::vector<std::string>{"a", "b", "c", "d"};
out = serde::serialize(a);
Test b;
ASSERT_NE(b.a(), a.a());
ASSERT_NE(b.b(), a.b());
ASSERT_OK(serde::deserialize(b, out));
ASSERT_EQ(b.a(), a.a());
ASSERT_EQ(b.b(), a.b());
}
TEST(TestSerde, Vector2) {
struct Item {
SERDE_STRUCT_FIELD(a, uint32_t{});
SERDE_STRUCT_FIELD(b, std::string{});
};
struct Vector {
SERDE_STRUCT_FIELD(vec, std::vector<Item>{});
};
Vector vec;
vec.vec.push_back({1, "one"});
vec.vec.push_back({2, "two"});
XLOGF(INFO, "vec is {}", vec);
}
TEST(TestSerde, Vector3) {
struct Item {
SERDE_STRUCT_FIELD(a, std::vector<uint32_t>{});
};
Item item;
item.a.push_back(2);
item.a.push_back(0);
item.a.push_back(2);
item.a.push_back(3);
auto out = serde::serialize(item);
ASSERT_EQ(out.size(), 18);
ASSERT_EQ(out[0], 17);
ASSERT_EQ(out[1], 4);
Item des;
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(item.a, des.a);
}
TEST(TestSerde, Set) {
struct Test {
SERDE_CLASS_FIELD(a, (std::set<int>{0, 1, 2}));
SERDE_CLASS_FIELD(b, (std::set<std::string>{"x", "y"}));
};
Test a;
auto out = serde::serialize(a);
ASSERT_EQ(out.length(), serde::serializeLength(a));
ASSERT_EQ(out.size(), 1 + 1 + 4 * 3 + 1 + 2 * (1 + 1));
a.a() = std::set<int>{0, 1, 0, 1};
a.b() = std::set<std::string>{"a", "b", "c", "d"};
out = serde::serialize(a);
Test b;
ASSERT_NE(b.a(), a.a());
ASSERT_NE(b.b(), a.b());
ASSERT_OK(serde::deserialize(b, out));
ASSERT_EQ(b.a(), a.a());
ASSERT_EQ(b.b(), a.b());
}
TEST(TestSerde, Map) {
struct Test {
SERDE_CLASS_FIELD(a, (std::map<int, int>{}));
SERDE_CLASS_FIELD(b, (robin_hood::unordered_map<std::string, std::string>{}));
};
Test a;
auto out = serde::serialize(a);
ASSERT_EQ(out.length(), serde::serializeLength(a));
ASSERT_EQ(out.size(), 1 + 1 + 1);
a.a()[1] = 2;
a.a()[3] = 4;
a.b()["hello"] = "world";
a.b()["nice to"] = "meet you";
out = serde::serialize(a);
ASSERT_EQ(out.size(), 1 + 1 + 4 * 4 + 1 + 4 * 1 + 5 + 5 + 7 + 8);
Test b;
ASSERT_NE(b.a(), a.a());
ASSERT_NE(b.b(), a.b());
ASSERT_OK(serde::deserialize(b, out));
ASSERT_EQ(b.a(), a.a());
ASSERT_EQ(b.b(), a.b());
}
TEST(TestSerde, Optional) {
struct Test {
SERDE_CLASS_FIELD(a, std::optional<std::string>{});
SERDE_CLASS_FIELD(b, std::optional<std::string>{"OK"});
};
Test a;
auto out = serde::serialize(a);
ASSERT_EQ(out.length(), serde::serializeLength(a));
ASSERT_EQ(out.size(), 1 + 1 + 1 + 1 + 2);
a.a() = "hello";
a.b() = std::nullopt;
out = serde::serialize(a);
ASSERT_EQ(out.size(), 1 + 1 + 1 + 5 + 1);
Test b;
ASSERT_NE(b.a(), a.a());
ASSERT_NE(b.b(), a.b());
ASSERT_OK(serde::deserialize(b, out));
ASSERT_EQ(b.a(), a.a());
ASSERT_EQ(b.b(), a.b());
}
TEST(TestSerde, Variant) {
struct Test {
SERDE_CLASS_FIELD(a, (std::variant<int, float, std::string>{}));
};
Test a;
auto out = serde::serialize(a);
ASSERT_EQ(out.length(), serde::serializeLength(a));
ASSERT_EQ(out.size(), 1 + 1 + 3 + 4);
a.a() = "hello";
out = serde::serialize(a);
ASSERT_EQ(out.size(), 1 + 1 + 12 + 1 + 5);
Test b;
ASSERT_NE(b.a(), a.a());
ASSERT_OK(serde::deserialize(b, out));
ASSERT_EQ(b.a(), a.a());
}
TEST(TestSerde, Compatibility) {
struct Test {
SERDE_CLASS_FIELD(a, std::optional<std::string>{});
SERDE_CLASS_FIELD(b, std::vector<int>{});
};
Test a;
a.a() = "ok";
a.b() = {1, 2, 3};
auto out = serde::serialize(a);
ASSERT_EQ(out.length(), serde::serializeLength(a));
ASSERT_EQ(out.length(), 1 + 1 + 1 + 2 + 1 + 4 * 3);
XLOGF(INFO, "out is {:02x}", fmt::join(out, ","));
size_t size = out.length();
for (size_t i = 1; i <= size; ++i) {
std::string_view view = out;
*reinterpret_cast<uint8_t *>(out.data()) = i - 1;
view = view.substr(0, i);
Test t;
if (i == 1) {
ASSERT_OK(serde::deserialize(t, view));
ASSERT_FALSE(t.a().has_value());
ASSERT_TRUE(t.b().empty());
} else if (i == 5) {
ASSERT_OK(serde::deserialize(t, view));
ASSERT_TRUE(t.a().has_value());
ASSERT_EQ(t.a().value(), "ok");
ASSERT_TRUE(t.b().empty());
} else if (i == size) {
ASSERT_OK(serde::deserialize(t, view));
ASSERT_TRUE(t.a().has_value());
ASSERT_EQ(t.a().value(), "ok");
ASSERT_FALSE(t.b().empty());
} else {
XLOGF(INFO, "length is {}", i);
ASSERT_FALSE(serde::deserialize(a, view));
}
}
}
struct Base {
SERDE_STRUCT_FIELD(x, int{});
SERDE_STRUCT_FIELD(y, int{});
};
struct Derived : Base {
SERDE_STRUCT_FIELD(z, int{});
};
struct Grandson : Derived {
SERDE_STRUCT_FIELD(x, int{});
SERDE_STRUCT_FIELD(y, int{});
SERDE_STRUCT_FIELD(z, int{});
};
namespace v1 {
struct Base {
SERDE_STRUCT_FIELD(x, int{});
};
struct Derived : Base {
SERDE_STRUCT_FIELD(z, int{});
};
struct Grandson : Derived {
SERDE_STRUCT_FIELD(x, int{});
SERDE_STRUCT_FIELD(y, int{});
};
}; // namespace v1
namespace v3 {
struct Base {
SERDE_STRUCT_FIELD(x, int{});
SERDE_STRUCT_FIELD(y, int{});
SERDE_STRUCT_FIELD(z, int{});
};
struct Derived : Base {
SERDE_STRUCT_FIELD(z, int{});
SERDE_STRUCT_FIELD(a, int{});
};
struct Grandson : Derived {
SERDE_STRUCT_FIELD(x, int{});
SERDE_STRUCT_FIELD(y, int{});
SERDE_STRUCT_FIELD(z, int{});
SERDE_STRUCT_FIELD(a, int{});
};
}; // namespace v3
static_assert(serde::count<Base>() == 2, "field count of Base must be 2");
static_assert(serde::count<Derived>() == 3, "field count of Derived must be 3");
static_assert(serde::count<Grandson>() == 6, "field count of Grandson must be 6");
static_assert(serde::count<v1::Grandson>() == 4);
static_assert(serde::getter<Grandson, 0>() != serde::getter<Grandson, 3>());
static_assert(serde::getter<Grandson, 1>() != serde::getter<Grandson, 4>());
static_assert(serde::getter<Grandson, 2>() != serde::getter<Grandson, 5>());
static_assert(serde::name<Base, 0>() == "x");
static_assert(serde::name<Base, 1>() == "y");
static_assert(serde::name<Derived, 0>() == "x");
static_assert(serde::name<Derived, 1>() == "y");
static_assert(serde::name<Derived, 2>() == "z");
static_assert(serde::name<Grandson, 0>() == "x");
static_assert(serde::name<Grandson, 1>() == "y");
static_assert(serde::name<Grandson, 2>() == "z");
static_assert(serde::name<Grandson, 3>() == "x");
static_assert(serde::name<Grandson, 4>() == "y");
static_assert(serde::name<Grandson, 5>() == "z");
TEST(TestSerde, Inherit) {
Grandson x;
x.Base::x = 10;
x.Base::y = 12;
x.Derived::z = 16;
x.Grandson::x = 20;
x.Grandson::y = 24;
x.Grandson::z = 28;
ASSERT_EQ(serde::count(x), 6);
ASSERT_EQ(serde::value<0>(x), 10);
ASSERT_EQ(serde::value<3>(x), 20);
int typeCount = 1;
refl::Helper::iterate<Grandson, true>([](auto t) { XLOGF(INFO, "field name: {}", t.name); },
[&]() {
++typeCount;
XLOGF(INFO, "type changed");
});
ASSERT_EQ(typeCount, 3);
auto out = serde::serialize(x);
Grandson o;
ASSERT_OK(serde::deserialize(o, out));
ASSERT_EQ(x.Base::x, o.Base::x);
ASSERT_EQ(x.Base::y, o.Base::y);
ASSERT_EQ(x.Derived::z, o.Derived::z);
ASSERT_EQ(x.Grandson::x, o.Grandson::x);
ASSERT_EQ(x.Grandson::y, o.Grandson::y);
ASSERT_EQ(x.Grandson::z, o.Grandson::z);
v1::Grandson p;
ASSERT_OK(serde::deserialize(p, out));
ASSERT_EQ(x.Base::x, p.Base::x);
ASSERT_EQ(x.Derived::z, p.Derived::z);
ASSERT_EQ(x.Grandson::x, p.Grandson::x);
ASSERT_EQ(x.Grandson::y, p.Grandson::y);
v3::Grandson q;
ASSERT_OK(serde::deserialize(q, out));
ASSERT_EQ(x.Base::x, q.Base::x);
ASSERT_EQ(x.Base::y, q.Base::y);
ASSERT_EQ(q.Base::z, 0);
ASSERT_EQ(x.Derived::z, q.Derived::z);
ASSERT_EQ(q.Derived::a, 0);
ASSERT_EQ(x.Grandson::x, q.Grandson::x);
ASSERT_EQ(x.Grandson::y, q.Grandson::y);
ASSERT_EQ(x.Grandson::z, q.Grandson::z);
ASSERT_EQ(q.Grandson::a, 0);
}
struct WithStringView {
SERDE_STRUCT_FIELD(msg, std::string_view{});
SERDE_STRUCT_FIELD(test, std::string_view{});
};
TEST(TestSerde, StringView) {
WithStringView view;
view.msg = "OK";
view.test = "test";
auto out = serde::serialize(view);
ASSERT_EQ(out.length(), serde::serializeLength(view));
WithStringView des;
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(view.msg, des.msg);
ASSERT_EQ(view.test, des.test);
ASSERT_LT(out.data(), des.msg.data());
ASSERT_LT(des.msg.data() + des.msg.length(), des.test.data());
ASSERT_LE(des.test.data() + des.test.length(), out.data() + out.length());
}
TEST(TestSerde, Result) {
{
Result<std::string> ser = "OK";
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
auto out = serde::serialize(ser);
ASSERT_EQ(out.size(), 1 + 1 + 2);
Result<std::string> des = makeError(Status::OK);
ASSERT_FALSE(des);
ASSERT_OK(serde::deserialize(des, out));
ASSERT_OK(des);
ASSERT_EQ(des, ser);
}
{
Result<std::string> ser = makeError(StatusCode::kInvalidArg);
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
auto out = serde::serialize(ser);
ASSERT_EQ(out.size(), 1 + 2 + 1);
Result<std::string> des = makeError(Status::OK);
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(des.error().code(), StatusCode::kInvalidArg);
ASSERT_TRUE(des.error().message().empty());
}
{
Result<std::string> ser = makeError(StatusCode::kInvalidArg, "TT");
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
auto out = serde::serialize(ser);
ASSERT_EQ(out.size(), 1 + 2 + 1 + 1 + 2);
Result<std::string> des = makeError(Status::OK);
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(des.error().code(), StatusCode::kInvalidArg);
ASSERT_EQ(des.error().message(), "TT");
}
{
Result<Void> ser = Void{};
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
auto out = serde::serialize(ser);
ASSERT_EQ(out.size(), 1);
Result<Void> des = makeError(Status::OK);
ASSERT_FALSE(des);
ASSERT_OK(serde::deserialize(des, out));
ASSERT_OK(des);
ASSERT_EQ(des, ser);
}
{
Result<Void> ser = makeError(StatusCode::kInvalidArg);
XLOGF(INFO, "result: {}", serde::toJsonString(ser));
auto out = serde::serialize(ser);
ASSERT_EQ(out.size(), 1 + 2 + 1);
Result<Void> des = makeError(Status::OK);
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(des.error().code(), StatusCode::kInvalidArg);
ASSERT_TRUE(des.error().message().empty());
}
}
TEST(TestSerde, NewDes) {
Demo ser;
ser.a() = 1;
ser.b() = 2;
ser.c() = "test";
auto out = serde::serialize(ser);
Demo des;
serde::In<std::string_view> in(out);
ASSERT_OK(serde::deserialize(des, in));
ASSERT_EQ(des.a(), 1);
ASSERT_EQ(des.b(), 2);
ASSERT_EQ(des.c(), "test");
}
TEST(TestSerde, NewDesJson) {
auto json = R"({
"a": 1,
"b": 2.0,
"c": "test"
})";
Demo des;
ASSERT_OK(serde::fromJsonString(des, json));
ASSERT_EQ(des.a(), 1);
ASSERT_EQ(des.b(), 2);
ASSERT_EQ(des.c(), "test");
}
TEST(TestSerde, NewDesToml) {
auto toml = R"(
a = 1
b = 2.0
c = "test"
)";
Demo des;
ASSERT_OK(serde::fromTomlString(des, toml));
ASSERT_EQ(des.a(), 1);
ASSERT_EQ(des.b(), 2);
ASSERT_EQ(des.c(), "test");
}
TEST(TestSerde, NewDesOptional) {
struct Foo {
SERDE_STRUCT_FIELD(num, std::optional<float>{});
SERDE_STRUCT_FIELD(str, std::optional<std::string>{});
};
{
Foo ser;
auto out = serde::serialize(ser);
Foo des;
serde::In<std::string_view> in(out);
ASSERT_OK(serde::deserialize(des, in));
ASSERT_FALSE(des.num.has_value());
ASSERT_FALSE(des.str.has_value());
}
{
Foo ser;
auto out = serde::toJsonString(ser);
XLOGF(INFO, "json is {}", out);
Foo des;
ASSERT_OK(serde::fromJsonString(des, out));
ASSERT_FALSE(des.num.has_value());
ASSERT_FALSE(des.str.has_value());
}
{
Foo ser;
auto out = serde::toTomlString(ser);
XLOGF(INFO, "toml is {}", out);
Foo des;
ASSERT_OK(serde::fromTomlString(des, out));
ASSERT_FALSE(des.num.has_value());
ASSERT_FALSE(des.str.has_value());
}
{
Foo des;
des.num = 20;
des.str = "Test";
ASSERT_OK(serde::fromJsonString(des, "{}"));
ASSERT_FALSE(des.num.has_value());
ASSERT_FALSE(des.str.has_value());
}
{
Foo des;
des.num = 20;
des.str = "Test";
ASSERT_OK(serde::fromTomlString(des, ""));
ASSERT_FALSE(des.num.has_value());
ASSERT_FALSE(des.str.has_value());
}
{
Foo ser;
ser.num = 27;
ser.str = "OK";
auto out = serde::serialize(ser);
Foo des;
serde::In<std::string_view> in(out);
ASSERT_OK(serde::deserialize(des, in));
ASSERT_TRUE(des.num.has_value());
ASSERT_EQ(ser.num, des.num);
ASSERT_TRUE(des.str.has_value());
ASSERT_EQ(ser.str, des.str);
}
{
Foo ser;
ser.num = 27;
ser.str = "OK";
auto out = serde::toJsonString(ser);
XLOGF(INFO, "json is {}", out);
Foo des;
ASSERT_OK(serde::fromJsonString(des, out));
ASSERT_TRUE(des.num.has_value());
ASSERT_EQ(ser.num, des.num);
ASSERT_TRUE(des.str.has_value());
ASSERT_EQ(ser.str, des.str);
}
{
Foo ser;
ser.num = 27;
ser.str = "OK";
auto out = serde::toTomlString(ser);
XLOGF(INFO, "toml is {}", out);
Foo des;
ASSERT_OK(serde::fromTomlString(des, out));
ASSERT_TRUE(des.num.has_value());
ASSERT_EQ(ser.num, des.num);
ASSERT_TRUE(des.str.has_value());
ASSERT_EQ(ser.str, des.str);
}
}
TEST(TestSerde, NewDesVariant) {
struct Foo {
SERDE_STRUCT_FIELD(num, (std::variant<int, double>{}));
SERDE_STRUCT_FIELD(val, (std::variant<std::string, Demo>{}));
};
{
Foo ser;
auto out = serde::serialize(ser);
Foo des;
serde::In<std::string_view> in(out);
ASSERT_OK(serde::deserialize(des, in));
ASSERT_EQ(des.num.index(), 0);
ASSERT_EQ(des.val.index(), 0);
}
{
Foo ser;
ser.num = 20.0;
ser.val = Demo{};
auto out = serde::serialize(ser);
Foo des;
ASSERT_EQ(des.num.index(), 0);
ASSERT_EQ(des.val.index(), 0);
serde::In<std::string_view> in(out);
ASSERT_OK(serde::deserialize(des, in));
ASSERT_EQ(des.num.index(), 1);
ASSERT_EQ(des.val.index(), 1);
}
{
auto json = R"({
"num": {
"type": "double",
"value": 70
},
"val": {
"type": "basic_string",
"value": "Test"
}
})";
Foo des;
ASSERT_OK(serde::fromJsonString(des, json));
ASSERT_EQ(des.num.index(), 1);
ASSERT_EQ(std::get<1>(des.num), 70.0);
ASSERT_EQ(des.val.index(), 0);
ASSERT_EQ(std::get<0>(des.val), "Test");
}
{
Foo ser;
ser.num = 70.0;
Demo demo;
demo.c() = "OK";
ser.val = demo;
auto out = serde::toJsonString(ser);
XLOGF(INFO, "json is {}", out);
Foo des;
ASSERT_OK(serde::fromJsonString(des, out));
ASSERT_EQ(des.num.index(), 1);
ASSERT_EQ(std::get<1>(des.num), 70.0);
ASSERT_EQ(des.val.index(), 1);
ASSERT_EQ(std::get<1>(des.val).c(), "OK");
}
}
TEST(TestSerde, NewDesContainer) {
struct Foo {
SERDE_STRUCT_FIELD(num, (std::vector<int>{}));
SERDE_STRUCT_FIELD(val, (std::set<std::string>{}));
SERDE_STRUCT_FIELD(map, (std::map<std::string, std::string>{}));
SERDE_STRUCT_FIELD(map2, (std::map<int, std::string>{}));
};
{
auto json = R"({
"num": [1, 2, 3, 4],
"val": ["A", "B"],
"map": {
"A": "1",
"B": "2"
},
"map2": {
"1": "A",
"2": "B"
}
})";
Foo des;
ASSERT_OK(serde::fromJsonString(des, json));
ASSERT_EQ(des.num.size(), 4);
ASSERT_EQ(des.val.size(), 2);
ASSERT_EQ(des.map.size(), 2);
ASSERT_EQ(des.map["A"], "1");
ASSERT_EQ(des.map["B"], "2");
ASSERT_EQ(des.map2.size(), 2);
ASSERT_EQ(des.map2[1], "A");
ASSERT_EQ(des.map2[2], "B");
}
}
TEST(TestSerde, NewDesEnum) {
enum class Foo { A, B, C };
Foo foo;
{
auto json = serde::toJsonString(Foo::B);
ASSERT_OK(serde::fromJsonString(foo, json));
ASSERT_EQ(foo, Foo::B);
}
ASSERT_FALSE(serde::fromJsonString(foo, "\"D\""));
}
TEST(TestSerde, UniquePtr) {
struct Foo {
SERDE_STRUCT_FIELD(value, std::unique_ptr<std::string>{});
};
struct Bar {
SERDE_STRUCT_FIELD(value, std::unique_ptr<Foo>{});
};
Bar bar;
XLOGF(INFO, "{}", bar);
{
Bar des;
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
ASSERT_EQ(des.value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
ASSERT_EQ(des.value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
ASSERT_EQ(des.value, nullptr);
}
bar.value = std::make_unique<Foo>();
XLOGF(INFO, "{}", bar);
{
Bar des;
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_EQ(des.value->value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_EQ(des.value->value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_EQ(des.value->value, nullptr);
}
bar.value->value = std::make_unique<std::string>("hello");
XLOGF(INFO, "{}", bar);
{
Bar des;
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_NE(des.value->value, nullptr);
ASSERT_EQ(*des.value->value, "hello");
}
{
Bar des;
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_NE(des.value->value, nullptr);
ASSERT_EQ(*des.value->value, "hello");
}
{
Bar des;
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_NE(des.value->value, nullptr);
ASSERT_EQ(*des.value->value, "hello");
}
}
TEST(TestSerde, SharedPtr) {
struct Foo {
SERDE_STRUCT_FIELD(value, std::shared_ptr<std::string>{});
};
struct Bar {
SERDE_STRUCT_FIELD(value, std::shared_ptr<Foo>{});
};
Bar bar;
XLOGF(INFO, "{}", bar);
{
Bar des;
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
ASSERT_EQ(des.value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
ASSERT_EQ(des.value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
ASSERT_EQ(des.value, nullptr);
}
bar.value = std::make_unique<Foo>();
XLOGF(INFO, "{}", bar);
{
Bar des;
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_EQ(des.value->value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_EQ(des.value->value, nullptr);
}
{
Bar des;
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_EQ(des.value->value, nullptr);
}
bar.value->value = std::make_unique<std::string>("hello");
XLOGF(INFO, "{}", bar);
{
Bar des;
ASSERT_OK(serde::deserialize(des, serde::serialize(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_NE(des.value->value, nullptr);
ASSERT_EQ(*des.value->value, "hello");
}
{
Bar des;
ASSERT_OK(serde::fromJsonString(des, serde::toJsonString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_NE(des.value->value, nullptr);
ASSERT_EQ(*des.value->value, "hello");
}
{
Bar des;
ASSERT_OK(serde::fromTomlString(des, serde::toTomlString(bar)));
ASSERT_NE(des.value, nullptr);
ASSERT_NE(des.value->value, nullptr);
ASSERT_EQ(*des.value->value, "hello");
}
}
TEST(TestSerde, AutoFallbackVariant) {
std::variant<String, Status> src = Status(StatusCode::kUnknown, "Unknown");
static_assert(!serde::is_auto_fallback_variant_v<decltype(src)>);
auto s = serde::serialize(src);
serde::AutoFallbackVariant<String> dst;
static_assert(serde::is_auto_fallback_variant_v<decltype(dst)>);
ASSERT_OK(serde::deserialize(dst, s));
auto *uvt = std::get_if<serde::UnknownVariantType>(&dst);
ASSERT_TRUE(uvt != nullptr);
ASSERT_EQ(uvt->type, "Status");
}
TEST(TestSerde, UserBufferAllocator) {
std::string ser = "hello world!";
auto len = serde::serializeLength(ser);
std::string buf(len, '\0');
serde::serializeToUserBuffer(ser, (uint8_t *)buf.data(), buf.size());
std::string der;
serde::deserialize(der, buf);
ASSERT_EQ(ser, der);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,146 @@
#include <folly/container/OrderedMap.h>
#include "common/net/ib/RDMABuf.h"
#include "common/serde/Serde.h"
#include "common/utils/Address.h"
#include "common/utils/Path.h"
#include "common/utils/Result.h"
#include "common/utils/Size.h"
#include "common/utils/StrongType.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
STRONG_TYPEDEF(uint32_t, ID);
enum class Type { A, B, C, D };
struct Demo {
bool operator==(const Demo &) const = default;
SERDE_STRUCT_FIELD(name, std::string{"Test"}, nullptr);
SERDE_STRUCT_FIELD(size, 1_MB, nullptr);
SERDE_STRUCT_FIELD(addr, net::Address{}, nullptr);
SERDE_STRUCT_FIELD(id, ID{12138}, nullptr);
SERDE_STRUCT_FIELD(type, Type::C, nullptr);
SERDE_STRUCT_FIELD(values, (std::set<ID>{ID{1}, ID{2}}), nullptr);
SERDE_STRUCT_FIELD(map, (std::map<std::string, Size>{{"a", 1_MB}, {"b", 2_MB}}), nullptr);
SERDE_STRUCT_FIELD(option, std::optional<std::string>("OK"), nullptr);
SERDE_STRUCT_FIELD(variant, (std::variant<int, std::string>("OK")), nullptr);
SERDE_STRUCT_FIELD(path, Path{"/a/b/c"}, nullptr);
};
TEST(TestSerde, ToToml) {
Demo demo;
XLOGF(INFO, "Toml: {}", serde::toTomlString(demo));
struct Demo2 {
SERDE_STRUCT_FIELD(one, Demo{});
SERDE_STRUCT_FIELD(two, Demo{});
};
XLOGF(INFO, "Toml: {}", serde::toTomlString(Demo2{}));
}
TEST(TestSerde, ToJson) {
Demo demo;
demo.option = std::nullopt; // count - 1
demo.variant = 100;
demo.path = "/x/y/z";
XLOGF(INFO, "Json: {}", serde::toJsonString(demo));
auto out = serde::serialize(demo);
Demo des;
ASSERT_NE(des.path, demo.path);
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(des, demo);
ASSERT_EQ(des.path, demo.path);
}
TEST(TestOrderedMap, Normal) {
folly::OrderedMap<int, int> map;
constexpr auto N = 1000;
for (auto i = 0; i < N; ++i) {
map[i] = i;
}
auto idx = 0;
for (auto [k, v] : map) {
ASSERT_EQ(k, idx);
ASSERT_EQ(v, idx);
++idx;
}
ASSERT_EQ(idx, N);
for (auto i = 0; i < N; i += 2) {
map.erase(i);
}
idx = 1;
for (auto [k, v] : map) {
ASSERT_EQ(k, idx);
ASSERT_EQ(v, idx);
idx += 2;
}
ASSERT_EQ(idx, N + 1);
for (auto i = 0; i < N; i += 2) {
map[i] = i;
}
ASSERT_EQ(map.size(), N);
idx = 1;
for (auto [k, v] : map) {
ASSERT_EQ(k, idx);
ASSERT_EQ(v, idx);
idx += 2;
if (idx == N + 1) {
idx = 0;
}
}
ASSERT_EQ(idx, N);
auto item = std::move(map);
ASSERT_EQ(item.size(), N);
ASSERT_EQ(map.size(), 0);
item.clear();
ASSERT_TRUE(item.empty());
}
TEST(TestSerde, RDMARemoteBuf) {
net::RDMARemoteBuf buf;
XLOGF(INFO, "buf is {}", serde::toJsonString(buf));
auto rkeys = buf.rkeys();
rkeys[0].devId = 1;
rkeys[0].rkey = 100;
rkeys[1].devId = 2;
rkeys[1].rkey = 200;
buf = net::RDMARemoteBuf(0x1000, 1024, rkeys);
XLOGF(INFO, "buf is {}", serde::toJsonString(buf));
auto out = serde::serialize(buf);
ASSERT_EQ(out.size(), 8 + 8 + 1 + 8 * 2); // addr + size + len + rkey * 2
net::RDMARemoteBuf des;
ASSERT_OK(serde::deserialize(des, out));
ASSERT_EQ(des, buf);
}
TEST(TestSerde, DoubleFormat) {
struct Foo {
SERDE_STRUCT_FIELD(value, 0ul);
};
Foo foo;
foo.value = ~0ul / 2;
XLOGF(INFO, "foo is {}", foo);
auto json = serde::toJsonString(foo);
Foo bar;
ASSERT_OK(serde::fromJsonString(bar, json));
ASSERT_EQ(bar.value, foo.value);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,425 @@
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Sleep.h>
#include <tuple>
#include <type_traits>
#include <utility>
#include "common/net/Client.h"
#include "common/net/Server.h"
#include "common/net/Transport.h"
#include "common/net/WriteItem.h"
#include "common/net/sync/Client.h"
#include "common/serde/CallContext.h"
#include "common/serde/ClientMockContext.h"
#include "common/serde/MessagePacket.h"
#include "common/serde/Serde.h"
#include "common/serde/Service.h"
#include "common/serde/Services.h"
#include "common/utils/Coroutine.h"
#include "common/utils/Reflection.h"
#include "common/utils/Thief.h"
#include "common/utils/TypeTraits.h"
#include "tests/GtestHelpers.h"
#include "tests/common/net/ib/SetupIB.h"
namespace hf3fs::serde::test {
namespace {
struct EchoReq {
SERDE_STRUCT_FIELD(value, std::string{});
};
struct EchoRsp {
SERDE_STRUCT_FIELD(value, std::string{});
};
struct FooReq {
SERDE_STRUCT_FIELD(value, 0);
};
struct FooRsp {
SERDE_STRUCT_FIELD(value, 0.0);
};
struct FourReq {
SERDE_STRUCT_FIELD(value, 0);
};
struct FourRsp {
SERDE_STRUCT_FIELD(value, 0);
};
SERDE_SERVICE(DemoService, 1) {
SERDE_SERVICE_METHOD(echo, 1, EchoReq, EchoRsp);
SERDE_SERVICE_METHOD(foo, 2, FooReq, FooRsp);
SERDE_SERVICE_METHOD(three, 3, int, int);
SERDE_SERVICE_METHOD(four, 4, FourReq, FourRsp);
};
struct ServiceImpl : public serde::ServiceWrapper<ServiceImpl, DemoService> {
CoTryTask<EchoRsp> echo(const EchoReq &req) {
cnt += 1;
EchoRsp rsp;
rsp.value = req.value;
co_return rsp;
}
CoTryTask<FooRsp> foo(const FooReq &req) {
cnt += 2;
FooRsp rsp;
rsp.value = req.value + 1;
co_return rsp;
}
CoTryTask<int> three(int req) {
cnt += 4;
XLOG(ERR, "req is {}", req);
co_return req * 2;
}
CoTryTask<FourRsp> four(const FourReq &req) {
cnt += 8;
FourRsp rsp;
rsp.value = req.value * req.value;
co_return rsp;
}
int cnt = 0;
};
static_assert(DemoService<>::kServiceName == "DemoService");
static_assert(DemoService<>::kServiceID == 1);
static_assert(refl::Helper::Size<ServiceImpl> == 4);
static_assert(!serde::SerdeType<ServiceImpl>);
struct FakeClient {
template <NameWrapper kServiceName,
NameWrapper kMethodName,
class Req,
class Rsp,
uint16_t ServiceID,
uint16_t MethodID>
CoTryTask<Rsp> call(const Req &, auto...) {
XLOGF(INFO, "call method {}:{}", ServiceID, MethodID);
co_return Rsp{};
}
};
class TestService : public net::test::SetupIB {};
TEST_F(TestService, Client) {
FakeClient ctx;
DemoService a;
folly::coro::blockingWait(a.echo(ctx, EchoReq{}));
folly::coro::blockingWait(a.foo(ctx, FooReq{}));
}
TEST_F(TestService, Server) {
ServiceImpl impl;
ASSERT_EQ(impl.cnt, 0);
folly::coro::blockingWait((impl.*refl::Helper::FieldInfo<ServiceImpl, 0>::method)(EchoReq{}));
ASSERT_EQ(impl.cnt, 1);
folly::coro::blockingWait((impl.*refl::Helper::FieldInfo<ServiceImpl, 1>::method)(FooReq{}));
ASSERT_EQ(impl.cnt, 3);
auto call = [&impl](auto type) {
using T = std::decay_t<decltype(type)>;
if constexpr (std::is_same_v<T, std::nullptr_t>) {
XLOGF(INFO, "not found!");
} else {
folly::coro::blockingWait((impl.*T::method)(typename T::ReqType{}));
}
};
callByIdx<refl::Helper::FieldInfoList<ServiceImpl>>(call, 0);
ASSERT_EQ(impl.cnt, 4);
callByIdx<refl::Helper::FieldInfoList<ServiceImpl>>(call, 1);
ASSERT_EQ(impl.cnt, 6);
callByIdx<refl::Helper::FieldInfoList<ServiceImpl>>(call, 4);
ASSERT_EQ(impl.cnt, 6);
}
struct FakeCallContext {
Result<std::string> handle(const std::string &str) {
RETURN_AND_LOG_ON_ERROR(serde::deserialize(recv_, str));
auto methodPointer = MethodExtractor<ServiceImpl, FakeCallContext>::get(recv_.methodId);
if (LIKELY(methodPointer != nullptr)) {
return (this->*methodPointer)();
} else {
XLOGF(INFO, "method not found!");
return makeError(RPCCode::kInvalidMethodID);
}
}
template <class FieldInfo>
Result<std::string> call() {
typename FieldInfo::ReqType req;
RETURN_AND_LOG_ON_ERROR(serde::deserialize(req, recv_.payload));
auto rsp = folly::coro::blockingWait((impl_.*FieldInfo::method)(req));
RETURN_AND_LOG_ON_ERROR(rsp);
MessagePacket send(*rsp);
return serde::serialize(send);
}
ServiceImpl impl_;
MessagePacket<> recv_;
};
class FakeServer {
public:
template <NameWrapper kServiceName,
NameWrapper kMethodName,
class Req,
class Rsp,
uint16_t ServiceID,
uint16_t MethodID>
CoTryTask<Rsp> call(const Req &req, auto...) {
XLOGF(INFO, "call method {}:{}", ServiceID, MethodID);
MessagePacket send(req);
send.serviceId = ServiceID;
send.methodId = MethodID;
auto bytes = serde::serialize(send);
FakeCallContext ctx;
auto result = ctx.handle(bytes);
CO_RETURN_ON_ERROR(result);
MessagePacket recv;
CO_RETURN_ON_ERROR(serde::deserialize(recv, *result));
Rsp rsp;
CO_RETURN_ON_ERROR(serde::deserialize(rsp, recv.payload));
co_return rsp;
}
};
TEST_F(TestService, ClientToServer) {
FakeServer ctx;
DemoService a;
{
EchoReq req;
req.value = "hello";
auto result = folly::coro::blockingWait(a.echo(ctx, req));
ASSERT_OK(result);
ASSERT_EQ(req.value, result->value);
}
{
FooReq req;
req.value = 100;
auto result = folly::coro::blockingWait(a.foo(ctx, req));
ASSERT_OK(result);
ASSERT_EQ(req.value + 1, result->value);
}
{
auto req = 100;
auto result = folly::coro::blockingWait(a.three(ctx, req));
ASSERT_OK(result);
ASSERT_EQ(req * 2, *result);
}
{
FourReq req;
req.value = 100;
auto result = folly::coro::blockingWait(a.four(ctx, req));
ASSERT_OK(result);
ASSERT_EQ(req.value * req.value, result->value);
}
}
static_assert(sizeof(CallContext::MethodType) == 16);
template <class Context = serde::CallContext>
struct RealService : public serde::ServiceWrapper<RealService<Context>, DemoService> {
CoTryTask<EchoRsp> echo(Context &, const EchoReq &req) {
cnt += 1;
EchoRsp rsp;
rsp.value = req.value;
co_return rsp;
}
CoTryTask<FooRsp> foo(Context &, const FooReq &req) {
cnt += 2;
FooRsp rsp;
rsp.value = req.value + 1;
co_return rsp;
}
CoTryTask<int> three(Context &, const int &req) {
cnt += 4;
co_return req * 2;
}
CoTryTask<FourRsp> four(Context &, const FourReq &) {
cnt += 8;
co_await folly::coro::sleep(std::chrono::milliseconds(10));
co_return makeError(StatusCode::kInvalidConfig);
}
std::atomic<int> cnt = 0;
};
TEST_F(TestService, Normal) {
Services services;
ASSERT_OK(services.addService(std::make_unique<RealService<>>(), false));
for (auto o = 0; o < 2; ++o) {
auto &service = services.getServiceById(RealService<>::kServiceID + o, false);
ASSERT_NE(service.getter, nullptr);
for (auto i = 0; i < 100; ++i) {
if (o == 0 && 1 <= i && i <= 4) {
ASSERT_NE(service.getter(i), nullptr);
} else {
ASSERT_EQ(service.getter(i), &CallContext::invalidId);
}
}
}
}
TEST_F(TestService, AddSerdeService) {
net::Server::Config config;
net::Server server(config);
ASSERT_OK(server.setup());
ASSERT_OK(server.addSerdeService(std::make_unique<RealService<>>()));
ASSERT_OK(server.start());
server.stopAndJoin();
}
TEST_F(TestService, CallContext) {
auto service = std::make_unique<RealService<>>();
auto pointer = service.get();
Services services;
ASSERT_OK(services.addService(std::move(service), false));
EchoReq req;
req.value = "hello";
MessagePacket send(req);
send.serviceId = 1;
send.methodId = 1;
auto bytes = serde::serialize(send);
MessagePacket<> recv;
ASSERT_OK(serde::deserialize(recv, bytes));
net::Server::Config config;
net::Server server(config);
ASSERT_OK(server.setup());
ASSERT_OK(server.start());
auto tr = net::Transport::create(net::Address{}, server.groups().front()->ioWorker());
{
ASSERT_EQ(pointer->cnt, 0);
CallContext ctx(recv, tr, services.getServiceById(recv.serviceId, false));
folly::coro::blockingWait(ctx.handle());
ASSERT_EQ(pointer->cnt, 1);
}
{
recv.methodId = 0;
CallContext ctx(recv, tr, services.getServiceById(recv.serviceId, false));
folly::coro::blockingWait(ctx.handle());
ASSERT_EQ(pointer->cnt, 1);
}
{
recv.serviceId = 0;
CallContext ctx(recv, tr, services.getServiceById(recv.serviceId, false));
folly::coro::blockingWait(ctx.handle());
ASSERT_EQ(pointer->cnt, 1);
}
server.stopAndJoin();
}
TEST_F(TestService, ClientContext) {
// 1. create service.
auto service = std::make_unique<RealService<>>();
auto pointer = service.get();
// 2. start server.
auto ctx = serde::ClientMockContext::create(std::move(service), net::Address::RDMA);
// 4. call client ctx.
{
DemoService stub;
EchoReq req{"hello"};
auto result = folly::coro::blockingWait(stub.echo(ctx, req));
ASSERT_OK(result);
ASSERT_EQ(result->value, req.value);
ASSERT_EQ(pointer->cnt, 1);
}
{
DemoService stub;
FooReq req{100};
auto result = folly::coro::blockingWait(stub.foo(ctx, req));
ASSERT_OK(result);
ASSERT_EQ(result->value, req.value + 1);
ASSERT_EQ(pointer->cnt, 3);
}
{
DemoService stub;
FooReq req{100};
auto result = stub.fooSync(ctx, req);
ASSERT_OK(result);
ASSERT_EQ(result->value, req.value + 1);
ASSERT_EQ(pointer->cnt, 5);
}
{
DemoService stub;
Timestamp timestamp{};
auto result = folly::coro::blockingWait(stub.three(ctx, 100, nullptr, &timestamp));
ASSERT_OK(result);
ASSERT_EQ(*result, 200);
ASSERT_EQ(pointer->cnt, 9);
serde::iterate(
[](std::string_view name, auto &value) {
if (value == 0) {
XLOGF(ERR, "{} is zero!", name);
ASSERT_NE(value, 0);
}
},
timestamp);
XLOGF(INFO, "timestamp is {}", timestamp);
}
{
DemoService stub;
FourReq req{100};
auto result = folly::coro::blockingWait(stub.four(ctx, req));
ASSERT_FALSE(result);
XLOGF(INFO, "result is error: {}", result.error());
}
{
DemoService stub;
FourReq req{100};
net::UserRequestOptions options;
options.timeout = 1_us;
auto result = folly::coro::blockingWait(stub.four(ctx, req, &options));
ASSERT_FALSE(result);
XLOGF(INFO, "result is error: {}", result.error());
}
}
TEST_F(TestService, CallSync) {
using namespace hf3fs::net;
// note: No IPoIB device in gitlab ci docker runner.
std::array<Address::Type, 4> networks{Address::LOCAL, Address::TCP /*, Address::IPoIB */};
for (auto network : networks) {
Server::Config serverConfig;
serverConfig.groups(0).set_network_type(network);
Server server_{serverConfig};
ASSERT_OK(server_.addSerdeService(std::make_unique<RealService<>>()));
ASSERT_OK(server_.setup());
ASSERT_OK(server_.start());
auto serverAddr = server_.groups().front()->addressList().front();
net::sync::Client::Config clientConfig;
net::sync::Client client_{clientConfig};
auto ctx = client_.serdeCtx(serverAddr);
EchoReq req{"hello"};
auto result = DemoService<>::echoSync(ctx, req);
ASSERT_OK(result);
ASSERT_EQ(result->value, req.value);
server_.stopAndJoin();
}
}
} // namespace
} // namespace hf3fs::serde::test

View File

@@ -0,0 +1,32 @@
#include <boost/intrusive/intrusive_fwd.hpp>
#include "common/serde/TypeName.h"
#include "common/utils/Nameof.hpp"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
static_assert(serde::type_name_v<int> == "int");
static_assert(serde::type_name_v<float> == "float");
static_assert(serde::type_name_v<double> == "double");
static_assert(serde::type_name_v<std::string> == "basic_string"); // template arguments are ignored
static_assert(serde::type_name_v<std::vector<int>> == "vector"); // template arguments are ignored
struct Foo {};
static_assert(serde::type_name_v<Foo> == "Foo");
struct Bar {
static constexpr std::string_view kTypeNameForSerde = "BAR";
};
static_assert(serde::type_name_v<Bar> == "BAR");
static_assert(serde::variant_type_names_v<std::variant<int, float>>[0] == "int");
static_assert(serde::variant_type_names_v<std::variant<int, float>>[1] == "float");
static_assert(serde::variant_type_names_v<std::variant<std::string, std::vector<int>>>[0] == "basic_string");
static_assert(serde::variant_type_names_v<std::variant<std::string, std::vector<int>>>[1] == "vector");
static_assert(serde::variant_type_names_v<std::variant<Foo, Bar>>[0] == "Foo");
static_assert(serde::variant_type_names_v<std::variant<Foo, Bar>>[1] == "BAR");
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,43 @@
#include <folly/json.h>
#include "common/serde/Serde.h"
#include "common/serde/Visit.h"
#include "common/utils/Nameof.hpp"
#include "tests/GtestHelpers.h"
template <>
class hf3fs::serde::VisitOut<void> {
public:
auto tableBegin() { XLOGF(INFO, "start a table"); }
void tableEnd() { XLOGF(INFO, "end a table"); }
void arrayBegin() { XLOGF(INFO, "start an array"); }
void arrayEnd() { XLOGF(INFO, "end an array"); }
void variantBegin() { XLOGF(INFO, "start a variant"); }
void variantEnd() { XLOGF(INFO, "end a variant"); }
void key(std::string_view key) { XLOGF(INFO, "key {}", key); }
void value(auto &&value) { XLOGF(INFO, "value {}", nameof::nameof_full_type<std::decay_t<decltype(value)>>()); }
};
namespace hf3fs::test {
namespace {
enum class Type {
A,
B,
C,
};
struct Foo {
SERDE_STRUCT_FIELD(val, double{});
SERDE_STRUCT_FIELD(type, Type::A);
SERDE_STRUCT_FIELD(num, (std::variant<int, double>{}));
SERDE_STRUCT_FIELD(vec, std::vector<std::string>{});
};
TEST(TestVist, Normal) {
serde::VisitOut<void> out;
serde::visit(Foo{}, out);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,35 @@
#include "common/utils/Address.h"
#include "common/utils/ConfigBase.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::net::test {
namespace {
TEST(TestAddress, Normal) {
constexpr std::string_view str = "IPoIB://192.168.1.1:8000";
auto addr = Address::fromString(str);
ASSERT_TRUE(addr.isTCP());
ASSERT_EQ(addr.ipStr(), "192.168.1.1");
ASSERT_EQ(addr.port, 8000);
ASSERT_EQ(addr.type, Address::IPoIB);
ASSERT_EQ(addr.str(), str);
addr = Address::fromString("tcp://127.0.0.1:8888");
ASSERT_TRUE(addr);
ASSERT_EQ(addr.type, Address::TCP);
ASSERT_EQ(addr.port, 8888);
}
static_assert(config::IsPrimitive<Address>, "address is not primitive");
auto kDefaultAddr = Address::from("tcp://192.168.1.1:8000").value();
class Config : public ConfigBase<Config> {
CONFIG_ITEM(addr, Address{kDefaultAddr});
};
TEST(TestAddress, Config) {
Config cfg;
ASSERT_EQ(cfg.toString(), "addr = 'TCP://192.168.1.1:8000'");
}
} // namespace
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,22 @@
#include <folly/experimental/coro/GtestHelpers.h>
#include "common/utils/AtomicValue.h"
namespace hf3fs::test {
namespace {
using namespace std::chrono_literals;
TEST(TestAtomicValue, Normal) {
AtomicValue<uint64_t> a;
ASSERT_EQ(a, 0);
a = 1;
ASSERT_EQ(a, 1);
AtomicValue<uint64_t> b = a;
ASSERT_EQ(b, 1);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,79 @@
#include <chrono>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <gtest/gtest.h>
#include <thread>
#include "common/utils/BoundedQueue.h"
#include "common/utils/Coroutine.h"
namespace hf3fs::test {
namespace {
using namespace std::chrono_literals;
TEST(TestBoundedQueue, Normal) {
BoundedQueue<int> queue(4);
ASSERT_TRUE(queue.empty());
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.enqueue(4);
ASSERT_TRUE(queue.full());
ASSERT_FALSE(queue.try_enqueue(5));
// 1. sync.
{
std::jthread dequeue([&] {
std::this_thread::sleep_for(100ms);
ASSERT_EQ(queue.dequeue(), 1);
});
auto start = std::chrono::steady_clock::now();
queue.enqueue(5);
auto elapsed = std::chrono::steady_clock::now() - start;
ASSERT_LE(50ms, elapsed);
ASSERT_LE(elapsed, 150ms);
}
// 2. async.
{
std::jthread dequeue([&] {
std::this_thread::sleep_for(100ms);
ASSERT_EQ(folly::coro::blockingWait(queue.co_dequeue()), 2);
});
auto start = std::chrono::steady_clock::now();
folly::coro::blockingWait(queue.co_enqueue(6));
auto elapsed = std::chrono::steady_clock::now() - start;
ASSERT_LE(50ms, elapsed);
ASSERT_LE(elapsed, 150ms);
}
// 3. try enqueue/dequeue.
{
ASSERT_FALSE(queue.try_enqueue(7));
ASSERT_EQ(queue.try_dequeue(), std::optional<int>{3});
ASSERT_TRUE(queue.try_enqueue(7));
int value;
ASSERT_TRUE(queue.try_dequeue(value));
ASSERT_EQ(value, 4);
ASSERT_EQ(queue.size(), 3);
queue.dequeue(value);
ASSERT_EQ(value, 5);
folly::coro::blockingWait(queue.co_dequeue(value));
ASSERT_EQ(value, 6);
ASSERT_EQ(queue.try_dequeue(), std::optional<int>{7});
ASSERT_EQ(queue.try_dequeue(), std::optional<int>{});
ASSERT_FALSE(queue.try_dequeue());
}
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,165 @@
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/Baton.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/experimental/coro/Timeout.h>
#include <gtest/gtest.h>
#include "common/utils/CoLockManager.h"
#include "common/utils/Duration.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
using namespace std::chrono_literals;
TEST(TestCoLockManager, Normal) {
CoLockManager lockManager;
{
folly::coro::Baton baton;
auto guard = lockManager.lock(baton, "A", "no wait");
folly::coro::blockingWait(guard.lock());
}
{
folly::coro::Baton baton;
auto guard = lockManager.lock(baton, "A", "no wait");
folly::coro::blockingWait(guard.lock());
}
folly::CPUThreadPoolExecutor executor(1);
folly::coro::co_invoke([&]() -> CoTask<void> {
folly::coro::Baton baton;
auto guard = lockManager.lock(baton, "B", "first");
CO_ASSERT_TRUE(guard.locked());
co_await guard.lock();
co_await folly::coro::sleep(200ms);
co_return;
})
.scheduleOn(&executor)
.start();
folly::coro::co_invoke([&]() -> CoTask<void> {
co_await folly::coro::sleep(100ms);
folly::coro::Baton baton;
auto start = RelativeTime::now();
auto guard = lockManager.lock(baton, "B", "second");
CO_ASSERT_FALSE(guard.locked());
co_await guard.lock();
auto elapsed = RelativeTime::now() - start;
CO_ASSERT_TRUE(70_ms <= elapsed);
CO_ASSERT_TRUE(elapsed <= 130_ms);
co_await folly::coro::sleep(200ms);
co_return;
})
.scheduleOn(&executor)
.start();
{
std::this_thread::sleep_for(300ms);
folly::coro::Baton baton;
auto start = RelativeTime::now();
auto guard = lockManager.lock(baton, "B", "third");
ASSERT_FALSE(guard.locked());
folly::coro::blockingWait(guard.lock());
auto elapsed = RelativeTime::now() - start;
ASSERT_NEAR(elapsed.asMs().count(), 100, 30);
}
}
TEST(TestCoLockManager, Concurrent) {
CoLockManager lockManager;
folly::CPUThreadPoolExecutor executor(16);
constexpr auto N = 20000;
auto cnt = 0;
auto current = 0;
folly::coro::Baton finish;
for (auto i = 0; i < N; ++i) {
folly::coro::co_invoke([&]() -> CoTask<void> {
folly::coro::Baton baton;
auto guard = lockManager.lock(baton, "A");
co_await guard.lock();
++current;
CO_ASSERT_EQ(current, 1);
if (++cnt == N) {
finish.post();
}
--current;
})
.scheduleOn(&executor)
.start();
}
auto result = folly::coro::blockingWait(
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5s)));
ASSERT_FALSE(result.hasException());
}
TEST(TestCoLockManager, Concurrent2) {
CoLockManager lockManager;
constexpr auto N = 20000;
auto cnt = 0;
auto current = 0;
folly::coro::Baton finish;
std::vector<std::thread> threads(16);
for (auto &thread : threads) {
thread = std::thread([&] {
for (auto i = 0; i < N / 16; ++i) {
folly::coro::Baton baton;
auto guard = lockManager.lock(baton, "A");
if (!guard.locked()) {
folly::coro::blockingWait(guard.lock());
}
++current;
ASSERT_EQ(current, 1);
if (++cnt == N) {
finish.post();
}
--current;
}
});
}
auto result = folly::coro::blockingWait(
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5s)));
ASSERT_FALSE(result.hasException());
for (auto &thread : threads) {
thread.join();
}
}
TEST(TestCoLockManager, Concurrent3) {
CoLockManager lockManager;
constexpr auto N = 20000;
auto cnt = 0;
auto current = 0;
folly::coro::Baton finish;
std::vector<std::thread> threads(16);
for (auto &thread : threads) {
thread = std::thread([&] {
for (auto i = 0; i < N / 16;) {
folly::coro::Baton baton;
auto guard = lockManager.tryLock(baton, "A");
if (!guard.locked()) {
continue;
}
++current;
ASSERT_EQ(current, 1);
if (++cnt == N) {
finish.post();
}
--current;
++i;
}
});
}
auto result = folly::coro::blockingWait(
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5s)));
ASSERT_FALSE(result.hasException());
for (auto &thread : threads) {
thread.join();
}
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,44 @@
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Timeout.h>
#include "common/utils/ConcurrencyLimiter.h"
#include "common/utils/Duration.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::net::test {
namespace {
TEST(TestConcurrencyLimiter, Normal) {
ConcurrencyLimiterConfig config;
ConcurrencyLimiter<std::string> limiter(config);
for (auto c = 1; c <= 4; c *= 2) {
config.set_max_concurrency(c);
constexpr auto N = 20000;
std::atomic<size_t> cnt = 0;
std::atomic<size_t> current = 0;
folly::coro::Baton finish;
std::vector<std::thread> threads(16);
for (auto &thread : threads) {
thread = std::thread([&] {
for (auto i = 0; i < N / 16; ++i) {
auto guard = folly::coro::blockingWait(limiter.lock("A"));
++current;
ASSERT_LE(current, c);
if (++cnt == N) {
finish.post();
}
--current;
}
});
}
auto result = folly::coro::blockingWait(
folly::coro::co_awaitTry(folly::coro::timeout([&]() -> CoTask<void> { co_await finish; }(), 5_s)));
ASSERT_FALSE(result.hasException());
for (auto &thread : threads) {
thread.join();
}
}
}
} // namespace
} // namespace hf3fs::net::test

View File

@@ -0,0 +1,522 @@
#include <chrono>
#include <folly/experimental/TestUtil.h>
#include <folly/logging/xlog.h>
#include <gtest/gtest.h>
#include <type_traits>
#include "common/utils/ConfigBase.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
enum class Channel { Red, Green, Blue };
class Config : public ConfigBase<Config> {
CONFIG_HOT_UPDATED_ITEM(string, "ing");
CONFIG_HOT_UPDATED_ITEM(channel, Channel::Red);
CONFIG_HOT_UPDATED_ITEM(optional, std::optional<std::string>{});
CONFIG_SECT(sect, {
CONFIG_HOT_UPDATED_ITEM(val, 100l, [](const int64_t &val) { return val < 200; });
CONFIG_HOT_UPDATED_ITEM(foo, "foo");
CONFIG_HOT_UPDATED_ITEM(score, 0.0);
CONFIG_HOT_UPDATED_ITEM(ok, false);
CONFIG_HOT_UPDATED_ITEM(succ, true);
CONFIG_SECT(sub, {
CONFIG_HOT_UPDATED_ITEM(val, 100l);
CONFIG_HOT_UPDATED_ITEM(foo, "foo");
});
});
CONFIG_SECT(vec, {
CONFIG_HOT_UPDATED_ITEM(int_vec, std::vector<int64_t>({1, 2}), [](const std::vector<int64_t> &v) {
return v.size() <= 2;
});
CONFIG_HOT_UPDATED_ITEM(str_vec, std::vector<std::string>({"foo", "foo"}));
CONFIG_HOT_UPDATED_ITEM(double_vec, std::vector<double>({1.0, 2.0}));
CONFIG_HOT_UPDATED_ITEM(bool_vec, std::vector<bool>({true, false}));
CONFIG_HOT_UPDATED_ITEM(enum_vec, std::vector<Channel>({Channel::Red, Channel::Green}));
});
};
static_assert(std::is_same_v<decltype(Config{}.sect().foo()), const std::string &>, "ok");
static_assert(std::is_same_v<decltype(Config{}.sect().val()), int64_t>, "ok");
static_assert(std::is_same_v<decltype(Config{}.vec().int_vec()), const std::vector<int64_t> &>, "ok");
class BigConfig : public ConfigBase<BigConfig> {
CONFIG_OBJ(a, Config);
CONFIG_OBJ(b, Config, [](Config &c) { c.set_string("replaced"); });
};
class ArrayConfig : public ConfigBase<ArrayConfig> {
CONFIG_OBJ_ARRAY(cfgs, Config, 4);
} array;
TEST(TestConfig, Normal) {
Config cfg;
ASSERT_EQ(cfg.string(), "ing");
ASSERT_FALSE(cfg.optional().has_value());
ASSERT_EQ(cfg.sect().foo(), "foo");
ASSERT_EQ(cfg.sect().val(), 100);
ASSERT_EQ(cfg.sect().ok(), false);
ASSERT_EQ(cfg.sect().succ(), true);
ASSERT_EQ(cfg.sect().sub().val(), 100);
toml::table result = toml::parse(R"(
optional = "has value"
[sect]
foo = "bar"
val = 123
score = 1
ok = true
[sect.sub]
val = 200
)");
ASSERT_EQ(cfg.sect().foo(), "foo");
ASSERT_EQ(cfg.sect().val(), 100);
ASSERT_EQ(cfg.sect().ok(), false);
ASSERT_EQ(cfg.sect().sub().val(), 100);
ASSERT_TRUE(cfg.update(result));
ASSERT_TRUE(cfg.optional().has_value());
ASSERT_EQ(cfg.optional().value(), "has value");
ASSERT_EQ(cfg.sect().foo(), "bar");
ASSERT_EQ(cfg.sect().val(), 123);
ASSERT_EQ(cfg.sect().ok(), true);
ASSERT_EQ(cfg.sect().sub().val(), 200);
ASSERT_TRUE(cfg.sect().set_val(150));
ASSERT_FALSE(cfg.sect().set_val(200));
// support copy.
auto other = cfg;
ASSERT_EQ(other.sect().foo(), "bar");
ASSERT_EQ(other.sect().val(), 150);
ASSERT_TRUE(cfg.update(result));
ASSERT_EQ(cfg.sect().val(), 123);
ASSERT_EQ(other.sect().val(), 150);
}
TEST(TestConfig, Vector) {
Config cfg;
ASSERT_TRUE(cfg.vec().enum_vec() == std::vector({Channel::Red, Channel::Green}));
toml::table result = toml::parse(R"(
[vec]
int_vec = [1, 2]
str_vec = ["foo", "bar"]
enum_vec = ["Red", "Blue"]
)");
ASSERT_TRUE(cfg.update(result));
std::vector<int64_t> expected_int_vec{1, 2};
ASSERT_TRUE(cfg.vec().int_vec() == expected_int_vec);
std::vector<std::string> expected_str_vec{"foo", "bar"};
ASSERT_TRUE(cfg.vec().str_vec() == expected_str_vec);
std::vector expected_enum_vec{Channel::Red, Channel::Blue};
ASSERT_TRUE(cfg.vec().enum_vec() == expected_enum_vec);
result = toml::parse(R"(
[vec]
int_vec = [1, 2, 3]
)");
ASSERT_FALSE(cfg.update(result));
result = toml::parse(R"(
[vec]
enum_vec = ["Yellow"]
)");
ASSERT_FALSE(cfg.update(result));
}
TEST(TestConfig, ParseValue) {
Config cfg;
// 1. empty config.
ASSERT_TRUE(cfg.update(toml::parse("")));
// 2. redundant entries.
ASSERT_FALSE(cfg.update(toml::parse(R"(
something = "else"
)")));
ASSERT_FALSE(cfg.update(toml::parse(R"(
[sect]
something = "else"
)")));
ASSERT_FALSE(cfg.update(toml::parse(R"(
[some]
thing = "else"
)")));
// 3. invalid values.
ASSERT_FALSE(cfg.update(toml::parse(R"(
[sect]
val = 200
)")));
// 4. invalid enum.
ASSERT_FALSE(cfg.update(toml::parse(R"(
channel = "Yellow"
)")));
}
TEST(TestConfig, Nested) {
BigConfig cfg;
ASSERT_EQ(cfg.a().channel(), Channel::Red);
ASSERT_EQ(cfg.a().string(), "ing");
ASSERT_EQ(cfg.b().string(), "replaced");
cfg.b().set_channel(Channel::Green);
ASSERT_EQ(cfg.a().channel(), Channel::Red);
ASSERT_EQ(cfg.b().channel(), Channel::Green);
BigConfig other;
ASSERT_TRUE(other.update(toml::parse(cfg.toString())));
ASSERT_EQ(other.a().channel(), Channel::Red);
ASSERT_EQ(other.b().channel(), Channel::Green);
}
TEST(TestConfig, ToToml) {
Config cfg;
cfg.set_string("set string with \"quotes\"");
cfg.sect().set_ok(false);
cfg.vec().set_str_vec({"1", "2", "3"});
cfg.set_channel(Channel::Blue);
XLOGF(INFO, "toString: {}", cfg.toString());
toml::table result = toml::parse(cfg.toString());
Config other;
ASSERT_TRUE(other.update(result));
ASSERT_EQ(cfg.string(), other.string());
ASSERT_EQ(cfg.sect().ok(), other.sect().ok());
ASSERT_EQ(cfg.vec().str_vec(), other.vec().str_vec());
ASSERT_EQ(cfg.toString(), other.toString());
ASSERT_EQ(cfg.channel(), other.channel());
}
// TEST(TestConfig, Init) {
// char *argvArr[] = {
// const_cast<char *>("./test"),
// const_cast<char *>("--config.channel=Blue"),
// const_cast<char *>("--config.string"),
// const_cast<char *>("a long string"),
// const_cast<char *>("--config.sect.val"),
// const_cast<char *>("123"),
// };
// int argc = ARRAY_SIZE(argvArr);
// Config cfg;
// auto *argv = argvArr; // decay char *[] to char **
// ASSERT_TRUE(cfg.init(&argc, &argv, false));
// ASSERT_EQ(cfg.channel(), Channel::Blue);
// ASSERT_EQ(cfg.string(), "a long string");
// ASSERT_EQ(cfg.sect().val(), 123);
// }
TEST(TestConfig, ArrayOfTable) {
ASSERT_EQ(array.cfgs_length(), 1u);
ASSERT_EQ(array.cfgs(0).channel(), Channel::Red);
toml::table table = toml::parse(R"(
[[cfgs]]
string = "A"
channel = "Blue"
[[cfgs]]
string = "B"
channel = "Red"
[cfgs.sect]
val = 121
)");
ASSERT_TRUE(array.update(table));
ASSERT_EQ(array.cfgs_length(), 2u);
ASSERT_EQ(array.cfgs(0).string(), "A");
ASSERT_EQ(array.cfgs(0).channel(), Channel::Blue);
ASSERT_EQ(array.cfgs(0).sect().val(), 100);
ASSERT_EQ(array.cfgs(1).string(), "B");
ASSERT_EQ(array.cfgs(1).channel(), Channel::Red);
ASSERT_EQ(array.cfgs(1).sect().val(), 121);
XLOGF(INFO, "toString: {}", array.toString());
}
TEST(TestConfig, CheckIsParsedFromString) {
Config cfg;
ASSERT_TRUE(cfg.find("string"));
ASSERT_TRUE(cfg.find("string").value()->isParsedFromString());
ASSERT_TRUE(cfg.find("channel"));
ASSERT_TRUE(cfg.find("channel").value()->isParsedFromString());
ASSERT_TRUE(cfg.find("optional"));
ASSERT_TRUE(cfg.find("optional").value()->isParsedFromString());
ASSERT_FALSE(cfg.find("sect"));
ASSERT_FALSE(cfg.find("not_found"));
ASSERT_TRUE(cfg.find("sect.foo"));
ASSERT_TRUE(cfg.find("sect.foo").value()->isParsedFromString());
ASSERT_TRUE(cfg.find("sect.ok"));
ASSERT_FALSE(cfg.find("sect.ok").value()->isParsedFromString());
}
TEST(TestConfig, HotUpdated) {
Config cfg;
auto now = std::chrono::steady_clock::now;
cfg.set_string(cfg.sect().foo());
std::jthread read([&] {
auto start = now();
while (now() - start <= std::chrono::milliseconds(100)) {
auto clone = cfg.clone();
ASSERT_EQ(clone.string(), clone.sect().foo());
clone.copy(cfg);
ASSERT_EQ(clone.string(), clone.sect().foo());
}
});
std::jthread update([&] {
auto start = now();
while (now() - start <= std::chrono::milliseconds(100)) {
ASSERT_TRUE(cfg.update(toml::parse(fmt::format(R"(
string = "{0}"
[sect]
foo = "{0}")",
now().time_since_epoch().count()))));
}
});
}
TEST(TestConfig, InlineTable) {
class Config : public ConfigBase<Config> {
CONFIG_HOT_UPDATED_ITEM(map, (std::map<std::string, int>{}));
CONFIG_HOT_UPDATED_ITEM(string_to_string, (std::map<std::string, std::string>{}));
} cfg;
ASSERT_TRUE(cfg.map().empty());
ASSERT_TRUE(cfg.string_to_string().empty());
toml::table table = toml::parse(R"(
map = { one = 1, two = 2 }
[string_to_string]
hello = 'world'
language = 'C++'
)");
ASSERT_TRUE(cfg.update(table));
ASSERT_EQ(cfg.map().at("one"), 1);
ASSERT_EQ(cfg.map().at("two"), 2);
ASSERT_EQ(cfg.string_to_string().at("hello"), "world");
ASSERT_EQ(cfg.string_to_string().at("language"), "C++");
XLOGF(INFO, "toString: {}", cfg.toString());
}
TEST(TestConfig, Set) {
class Config : public ConfigBase<Config> {
CONFIG_HOT_UPDATED_ITEM(set, std::set<std::string>{});
} cfg;
ASSERT_TRUE(cfg.set().empty());
toml::table table = toml::parse(R"(
set = ["one", "two"]
)");
ASSERT_TRUE(cfg.update(table));
ASSERT_EQ(cfg.set().count("one"), 1);
ASSERT_EQ(cfg.set().count("two"), 1);
ASSERT_EQ(cfg.set().count("three"), 0);
XLOGF(INFO, "toString: {}", cfg.toString());
ASSERT_EQ(cfg.toString(), "set = [ 'one', 'two' ]");
}
TEST(TestConfig, AtomicallyUpdate) {
std::string_view str = R"(
[sect]
val = 123
)";
Config cfg;
ASSERT_TRUE(cfg.atomicallyUpdate(str));
ASSERT_EQ(cfg.sect().val(), 123);
cfg.sect().set_val(100);
folly::test::TemporaryFile file;
::write(file.fd(), str.data(), str.length());
ASSERT_TRUE(cfg.atomicallyUpdate(file.path()));
ASSERT_EQ(cfg.sect().val(), 123);
}
TEST(TestConfig, VariantType) {
class Config : public ConfigBase<Config> {
CONFIG_SECT(type1, {
CONFIG_HOT_UPDATED_ITEM(a, 0);
CONFIG_HOT_UPDATED_ITEM(b, 0);
CONFIG_HOT_UPDATED_ITEM(c, 0);
});
CONFIG_SECT(type2, {
CONFIG_HOT_UPDATED_ITEM(x, 0);
CONFIG_HOT_UPDATED_ITEM(y, 0);
CONFIG_HOT_UPDATED_ITEM(z, 0);
});
CONFIG_VARIANT_TYPE("type1");
};
Config config;
XLOGF(INFO, "{}", config.toString());
ASSERT_OK(config.validate());
ASSERT_TRUE(config.set_type("type2"));
XLOGF(INFO, "{}", config.toString());
ASSERT_OK(config.validate());
ASSERT_FALSE(config.set_type("type3"));
}
TEST(TestConfig, UpdateCallback) {
Config config;
auto cnt = 0;
auto guard = config.addCallbackGuard([&] { ++cnt; });
ASSERT_EQ(cnt, 0);
ASSERT_OK(config.update(toml::parse(R"( string = "else" )")));
ASSERT_EQ(cnt, 1);
guard->dismiss();
ASSERT_OK(config.update(toml::parse(R"( string = "else" )")));
ASSERT_EQ(cnt, 1);
guard = config.addCallbackGuard();
guard->setCallback([&] { cnt += 7; });
ASSERT_OK(config.update(toml::parse(R"( string = "else" )")));
ASSERT_EQ(cnt, 8);
}
TEST(TestConfig, HotUpdatedMacro) {
class Config : public ConfigBase<Config> {
CONFIG_SECT(cold, {
CONFIG_ITEM(a, 0);
CONFIG_ITEM(b, (std::vector<int>{1, 2, 3}));
CONFIG_ITEM(c, (std::map<std::string, int>{{"one", 1}, {"two", 2}}));
});
CONFIG_SECT(hot, {
CONFIG_HOT_UPDATED_ITEM(a, 0);
CONFIG_HOT_UPDATED_ITEM(b, (std::vector<int>{1, 2, 3}));
CONFIG_HOT_UPDATED_ITEM(c, (std::map<std::string, int>{{"one", 1}, {"two", 2}}));
});
};
Config config;
// hot update but keep value, succ.
ASSERT_OK(config.update(toml::parse(R"(
[cold]
a = 0
b = [1, 2, 3]
c = { one = 1, two = 2 }
)")));
// hot update and change value, fail.
ASSERT_FALSE(config.update(toml::parse(R"(
[cold]
a = 1
)")));
ASSERT_FALSE(config.update(toml::parse(R"(
[cold]
b = [1, 2]
)")));
ASSERT_FALSE(config.update(toml::parse(R"(
[cold]
c = { one = 1 }
)")));
// not a hot update. succ.
ASSERT_OK(config.update(toml::parse(R"(
[cold]
a = 1
)"),
false));
ASSERT_OK(config.update(toml::parse(R"(
[cold]
b = [1, 2]
)"),
false));
ASSERT_OK(config.update(toml::parse(R"(
[cold]
c = { one = 1 }
)"),
false));
ASSERT_EQ(config.cold().a(), 1);
ASSERT_EQ(config.cold().b(), (std::vector<int>{1, 2}));
ASSERT_EQ(config.cold().c(), (std::map<std::string, int>{{"one", 1}}));
// support hot updated. succ.
ASSERT_OK(config.update(toml::parse(R"(
[hot]
a = 0
b = [1, 2, 3]
c = { one = 1, two = 2 }
)")));
ASSERT_OK(config.update(toml::parse(R"(
[hot]
a = 1
)")));
ASSERT_OK(config.update(toml::parse(R"(
[hot]
b = [1, 2]
)")));
ASSERT_OK(config.update(toml::parse(R"(
[hot]
c = { one = 1 }
)")));
ASSERT_OK(config.update(toml::parse(R"(
[hot]
a = 1
)"),
false));
ASSERT_OK(config.update(toml::parse(R"(
[hot]
b = [1, 2]
)"),
false));
ASSERT_OK(config.update(toml::parse(R"(
[hot]
c = { one = 1 }
)"),
false));
}
TEST(TestConfig, DiffWith) {
auto stringfy = [](const config::IConfig::ItemDiff &diff) {
return fmt::format("{}: {} -> {}", diff.key, diff.left, diff.right);
};
config::IConfig::ItemDiff diffs[3];
Config c0;
Config c1;
ASSERT_EQ(c0.diffWith(c1, std::span(diffs)), 0);
c1.set_optional("nonnull");
c1.sect().set_foo("abc");
c1.sect().sub().set_val(101);
auto diffCnt = c0.diffWith(c1, std::span(diffs));
ASSERT_EQ(diffCnt, 3);
ASSERT_EQ(stringfy(diffs[0]), "optional: nullopt -> 'nonnull'");
ASSERT_EQ(stringfy(diffs[1]), "sect.foo: 'foo' -> 'abc'");
ASSERT_EQ(stringfy(diffs[2]), "sect.sub.val: 100 -> 101");
c1.sect().sub().set_foo("");
ASSERT_EQ(c0.diffWith(c1, std::span(diffs)), 3);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,42 @@
#include <folly/executors/CPUThreadPoolExecutor.h>
#include "common/utils/CoroSynchronized.h"
#include "common/utils/CountDownLatch.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
folly::CPUThreadPoolExecutor &getExecutor() {
static folly::CPUThreadPoolExecutor executor(4, std::make_shared<folly::NamedThreadFactory>("Test"));
return executor;
}
TEST(CoroSynchronizedTest, testInc) {
CoroSynchronized<int64_t> count(0);
constexpr int64_t countPerAdder = 10000;
constexpr int64_t adderCount = 10;
CountDownLatch latch(adderCount);
auto adder = [&]() -> CoTask<void> {
for (int64_t i = 0; i < countPerAdder; ++i) {
auto ptr = co_await count.coLock();
++*ptr;
}
latch.countDown();
};
for (int64_t i = 0; i < adderCount; ++i) {
adder().scheduleOn(&getExecutor()).start();
}
auto result = folly::coro::blockingWait([&]() -> CoTask<int64_t> {
co_await latch.wait();
auto ptr = co_await count.coSharedLock();
co_return *ptr;
}());
ASSERT_EQ(result, adderCount * countPerAdder);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,49 @@
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include "common/utils/CoroutinesPool.h"
#include "common/utils/CountDownLatch.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
void testCoroutinesPool(bool enableWorkStealing) {
struct Job {
int cnt = 0;
};
folly::CPUThreadPoolExecutor executor(4);
CoroutinesPool<Job>::Config config;
config.set_enable_work_stealing(enableWorkStealing);
CoroutinesPool<Job> pool(config, &executor);
SCOPE_EXIT { pool.stopAndJoin(); };
int n = 100;
CountDownLatch latch(2 * n);
std::atomic<int> sum = 0;
pool.start([&sum, &latch](Job job) -> CoTask<void> {
sum += job.cnt;
latch.countDown();
co_return;
});
for (auto i = 0; i < n; ++i) {
pool.enqueueSync(Job{i + 1});
folly::coro::blockingWait(pool.enqueue(Job{i}));
}
folly::coro::blockingWait(latch.wait());
ASSERT_EQ(sum, 10000);
}
TEST(TestCoroutinesPool, Normal) { testCoroutinesPool(false); }
TEST(TestCoroutinesPool, WorkStealing) { testCoroutinesPool(true); }
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,52 @@
#include <folly/experimental/coro/BlockingWait.h>
#include <gtest/gtest.h>
#include "common/utils/DefaultRetryStrategy.h"
namespace hf3fs::tests {
namespace {
using namespace std::chrono_literals;
struct MockSleeper {
CoTask<void> operator()(std::chrono::milliseconds d) {
records.push_back(d);
co_return;
}
std::vector<std::chrono::milliseconds> records;
};
DefaultRetryStrategy<MockSleeper> getDefaultRetryStrategy() {
return DefaultRetryStrategy<MockSleeper>(RetryConfig{10ms, 100ms, 5});
}
TEST(DefaultRetryStrategy, basic) {
folly::coro::blockingWait([&]() -> CoTask<void> {
{
auto retryStrategy = getDefaultRetryStrategy();
// kDataCorruption is unretryable
auto r = co_await retryStrategy.onError(Status(StatusCode::kDataCorruption));
EXPECT_TRUE(r.hasError());
EXPECT_EQ(r.error().code(), StatusCode::kDataCorruption);
EXPECT_EQ(retryStrategy.getSleeper().records.size(), 0);
}
{
auto retryStrategy = getDefaultRetryStrategy();
// kThrottled is retryable
for (int i = 0; i < 5; ++i) {
auto r = co_await retryStrategy.onError(Status(TransactionCode::kThrottled));
EXPECT_TRUE(!r.hasError());
}
{
auto r = co_await retryStrategy.onError(Status(TransactionCode::kThrottled));
EXPECT_TRUE(r.hasError());
EXPECT_EQ(r.error().code(), TransactionCode::kThrottled);
}
std::vector<std::chrono::milliseconds> expected = {10ms, 20ms, 40ms, 80ms, 100ms};
EXPECT_EQ(retryStrategy.getSleeper().records, expected);
}
}());
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,121 @@
#include "common/net/Allocator.h"
#include "common/utils/DownwardBytes.h"
#include "common/utils/Size.h"
#include "tests/GtestHelpers.h"
namespace hf3fs {
namespace {
std::vector<std::pair<size_t, bool>> logger;
struct Allocator {
public:
static constexpr size_t kDefaultSize = 1_MB;
static std::uint8_t *allocate(size_t size) {
size = std::max(size, kDefaultSize);
logger.emplace_back(size, true);
return new uint8_t[size];
}
static void deallocate(const uint8_t *buf, size_t size) {
size = std::max(size, kDefaultSize);
logger.emplace_back(size, false);
delete[] buf;
}
};
TEST(TestDownwardBytes, Normal) {
ASSERT_EQ(logger.size(), 0);
{
DownwardBytes<Allocator> bytes;
ASSERT_EQ(bytes.capacity(), 1_MB);
ASSERT_EQ(logger.size(), 1);
ASSERT_EQ(logger.back().first, 1_MB);
ASSERT_TRUE(logger.back().second);
}
ASSERT_EQ(logger.size(), 2);
ASSERT_EQ(logger.back().first, 1_MB);
ASSERT_FALSE(logger.back().second);
{
DownwardBytes<Allocator> bytes;
auto old = bytes.data();
ASSERT_EQ(bytes.capacity(), 1_MB);
ASSERT_EQ(logger.size(), 3);
ASSERT_EQ(logger.back().first, 1_MB);
ASSERT_TRUE(logger.back().second);
std::vector<uint8_t> empty(512_KB, 'A');
bytes.append(empty.data(), empty.size());
ASSERT_EQ(bytes.data() + 512_KB, old);
ASSERT_EQ(bytes.data()[0], 'A');
ASSERT_EQ(bytes.data()[512_KB - 1], 'A');
ASSERT_EQ(bytes.capacity(), 1_MB);
ASSERT_EQ(bytes.size(), 512_KB);
ASSERT_EQ(logger.size(), 3);
std::fill(empty.begin(), empty.end(), 'B');
bytes.append(empty.data(), empty.size());
ASSERT_EQ(bytes.data() + 1_MB, old);
ASSERT_EQ(bytes.data()[0], 'B');
ASSERT_EQ(bytes.data()[512_KB - 1], 'B');
ASSERT_EQ(bytes.data()[512_KB], 'A');
ASSERT_EQ(bytes.data()[1_MB - 1], 'A');
ASSERT_EQ(bytes.capacity(), 1_MB);
ASSERT_EQ(bytes.size(), 1_MB);
ASSERT_EQ(logger.size(), 3);
std::fill(empty.begin(), empty.end(), 'C');
bytes.append(empty.data(), 1);
ASSERT_NE(bytes.data() + 1_MB + 1, old);
ASSERT_EQ(bytes.data()[0], 'C');
ASSERT_EQ(bytes.data()[1], 'B');
ASSERT_EQ(bytes.data()[512_KB], 'B');
ASSERT_EQ(bytes.data()[512_KB + 1], 'A');
ASSERT_EQ(bytes.data()[1_MB], 'A');
ASSERT_EQ(bytes.capacity(), 2_MB);
ASSERT_EQ(bytes.size(), 1_MB + 1);
ASSERT_EQ(logger.size(), 5);
ASSERT_EQ(logger[3].first, 2_MB);
ASSERT_TRUE(logger[3].second);
ASSERT_EQ(logger[4].first, 1_MB);
ASSERT_FALSE(logger[4].second);
uint32_t offset, capacity;
auto buf = bytes.release(offset, capacity);
ASSERT_EQ(logger.size(), 5);
ASSERT_EQ(bytes.data(), nullptr);
ASSERT_EQ(bytes.size(), 0);
ASSERT_EQ(bytes.capacity(), 0);
ASSERT_EQ(offset, 1_MB - 1);
ASSERT_EQ(capacity, 2_MB);
ASSERT_EQ(buf[offset], 'C');
Allocator::deallocate(buf, capacity);
}
ASSERT_EQ(logger.size(), 6);
ASSERT_EQ(logger.back().first, 2_MB);
ASSERT_FALSE(logger.back().second);
{
DownwardBytes<Allocator> bytes;
ASSERT_EQ(bytes.capacity(), 1_MB);
ASSERT_EQ(logger.size(), 7);
ASSERT_EQ(logger.back().first, 1_MB);
ASSERT_TRUE(logger.back().second);
std::vector<uint8_t> empty(2_MB, 'A');
bytes.append(empty.data(), empty.size());
ASSERT_EQ(logger.size(), 9);
ASSERT_EQ(logger.back().first, 1_MB);
ASSERT_FALSE(logger.back().second);
ASSERT_EQ(logger[7].first, 2_MB);
ASSERT_TRUE(logger[7].second);
}
ASSERT_EQ(logger.size(), 10);
ASSERT_EQ(logger.back().first, 2_MB);
ASSERT_FALSE(logger.back().second);
}
} // namespace
} // namespace hf3fs

View File

@@ -0,0 +1,76 @@
#include <folly/logging/xlog.h>
#include "common/utils/ConfigBase.h"
#include "common/utils/Duration.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
static_assert(config::IsPrimitive<Duration>, "duration is not primitive");
class Config : public ConfigBase<Config> {
CONFIG_HOT_UPDATED_ITEM(c0, Duration::from("0s").value());
CONFIG_HOT_UPDATED_ITEM(c1, 0_ns);
};
TEST(Duration, testDefaultConstruct) {
Duration config;
ASSERT_EQ(config.asMs(), std::chrono::milliseconds{0});
}
TEST(Duration, testParse) {
Duration c0 = Duration::from("100ns").value();
ASSERT_EQ(c0.toString(), "100ns");
Duration c1 = Duration::from("2us").value();
ASSERT_EQ(c1.toString(), "2us");
Duration c2 = Duration::from("1001ms").value();
ASSERT_EQ(c2.toString(), "1s 1ms");
Duration c3 = Duration::from("45s ").value();
ASSERT_EQ(c3.toString(), "45s");
Duration c5 = Duration::from("08h").value();
ASSERT_EQ(c5.toString(), "8h");
Duration c6 = Duration::from("1day").value();
ASSERT_EQ(c6.toString(), "1day");
ASSERT_FALSE(Duration::from("10mon"));
ASSERT_FALSE(Duration::from(""));
ASSERT_FALSE(Duration::from("100"));
ASSERT_FALSE(Duration::from("ns"));
ASSERT_FALSE(Duration::from("1.1s"));
ASSERT_ERROR(Duration::from("1ns2ms"), StatusCode::kInvalidConfig);
ASSERT_ERROR(Duration::from("1ns2ns"), StatusCode::kInvalidConfig);
ASSERT_ERROR(Duration::from("1ns2k"), StatusCode::kInvalidConfig);
ASSERT_ERROR(Duration::from("1ms2.1us"), StatusCode::kInvalidConfig);
ASSERT_ERROR(Duration::from("ms2us"), StatusCode::kInvalidConfig);
ASSERT_ERROR(Duration::from("1day2sec"), StatusCode::kInvalidConfig);
ASSERT_EQ(Duration::from("1day 2s")->toString(), "1day 2s");
ASSERT_EQ(Duration::from("1day 2h 2001 ms")->toString(), "1day 2h 2s 1ms");
}
TEST(Duration, testConfig) {
Config cfg;
ASSERT_EQ(cfg.c0(), Duration::from("0s").value());
ASSERT_EQ(cfg.c0(), cfg.c1());
cfg.set_c0(Duration::from("10ms").value());
ASSERT_EQ(cfg.c0(), Duration::from("10ms").value());
toml::table result = toml::parse(R"(
c0 = "5s"
c1 = "10min"
)");
ASSERT_TRUE(cfg.update(result));
ASSERT_EQ(cfg.c0(), Duration::from("5s").value());
ASSERT_EQ(cfg.c1(), Duration::from("10min").value());
XLOGF(INFO, "toString: {}", cfg.toString());
result = toml::parse(cfg.toString());
Config other;
ASSERT_TRUE(other.update(result));
ASSERT_EQ(cfg.c0(), other.c0());
ASSERT_EQ(cfg.c1(), other.c1());
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,77 @@
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/Baton.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/Sleep.h>
#include "common/utils/Duration.h"
#include "common/utils/DynamicCoroutinesPool.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
TEST(TestDynamicCoroutinesPool, Normal) {
DynamicCoroutinesPool::Config config;
config.set_coroutines_num(8);
DynamicCoroutinesPool pool(config);
ASSERT_OK(pool.start());
ASSERT_OK(pool.stopAndJoin());
DynamicCoroutinesPool{config};
}
TEST(TestDynamicCoroutinesPool, ManyTasks) {
constexpr auto N = 200;
constexpr auto M = 64;
DynamicCoroutinesPool::Config config;
config.set_coroutines_num(M);
DynamicCoroutinesPool pool(config);
ASSERT_OK(pool.start());
folly::coro::Baton baton;
std::atomic<uint32_t> cnt{};
std::atomic<uint32_t> current{};
for (auto i = 0; i < N; ++i) {
pool.enqueue(folly::coro::co_invoke([&]() -> CoTask<void> {
auto now = ++current;
CO_ASSERT_LE(now, M);
co_await folly::coro::sleep(50_ms);
if (++cnt == N) {
baton.post();
}
--current;
}));
}
folly::coro::blockingWait(baton);
}
TEST(TestDynamicCoroutinesPool, HotUpdated) {
constexpr auto N = 64;
DynamicCoroutinesPool::Config config;
config.set_coroutines_num(1);
DynamicCoroutinesPool pool(config);
ASSERT_OK(pool.start());
folly::coro::Baton baton;
std::atomic<uint32_t> cnt{};
std::atomic<uint32_t> current{};
for (auto i = 0; i < N; ++i) {
pool.enqueue(folly::coro::co_invoke([&]() -> CoTask<void> {
auto now = ++current;
config.set_coroutines_num(now + 1);
config.update(toml::table{}, true);
co_await folly::coro::sleep(50_ms);
if (++cnt == N) {
baton.post();
}
--current;
}));
}
folly::coro::blockingWait(baton);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,225 @@
#include <atomic>
#include <chrono>
#include <fmt/core.h>
#include <folly/Random.h>
#include <folly/concurrency/UnboundedQueue.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/executors/IOThreadPoolExecutor.h>
#include <folly/experimental/coro/AsyncGenerator.h>
#include <folly/experimental/coro/Baton.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/BoundedQueue.h>
#include <folly/experimental/coro/CurrentExecutor.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/ScopeExit.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/experimental/coro/Task.h>
#include <folly/experimental/coro/UnboundedQueue.h>
#include <folly/futures/Future.h>
#include <folly/io/async/Request.h>
#include <gtest/gtest.h>
#include <optional>
#include <type_traits>
#include <vector>
#include "common/utils/Coroutine.h"
#include "common/utils/FaultInjection.h"
namespace hf3fs::test {
namespace {
TEST(TestFaultInjection, Basic) {
static constexpr size_t kLoops = 100000;
auto fi = FaultInjection::get();
ASSERT_EQ(fi, nullptr);
for (size_t i = 0; i < kLoops; i++) {
auto inject = FAULT_INJECTION();
ASSERT_FALSE(inject);
}
FAULT_INJECTION_SET(1, -1); // 1% probility and unlimited times
ASSERT_EQ(FaultInjection::get()->getProbability(), 1);
ASSERT_EQ(FaultInjectionFactor::get(), 1);
size_t cnt = 0;
for (size_t i = 0; i < kLoops; i++) {
if (FAULT_INJECTION()) {
cnt++;
}
}
ASSERT_NE(cnt, 0);
std::cout << "Inject " << cnt << " faults during " << kLoops << " loops, " << (double)cnt / kLoops * 100
<< "%, expect 1%" << std::endl;
{
// disable fault injection in current scope
FAULT_INJECTION_SET(0, -1);
size_t cnt = 0;
for (size_t i = 0; i < kLoops; i++) {
if (FAULT_INJECTION()) {
cnt++;
}
}
ASSERT_EQ(cnt, 0);
}
ASSERT_EQ(FaultInjection::get()->getProbability(), 1);
ASSERT_EQ(FaultInjectionFactor::get(), 1);
// test set factor scope.
{
FAULT_INJECTION_SET_FACTOR(2);
ASSERT_EQ(FaultInjectionFactor::get(), 2);
{
FAULT_INJECTION_SET_FACTOR(3);
ASSERT_EQ(FaultInjectionFactor::get(), 3);
}
ASSERT_EQ(FaultInjectionFactor::get(), 2);
FAULT_INJECTION_SET_FACTOR(4);
ASSERT_EQ(FaultInjectionFactor::get(), 4);
}
ASSERT_EQ(FaultInjectionFactor::get(), 1);
{
FAULT_INJECTION_SET_FACTOR(10);
ASSERT_EQ(FaultInjectionFactor::get(), 10);
size_t cnt = 0;
for (size_t i = 0; i < kLoops; i++) {
if (FAULT_INJECTION()) {
cnt++;
}
}
ASSERT_NE(cnt, 0);
std::cout << "Set factor of current scope to 10, inject " << cnt << " faults during " << kLoops << " loops, "
<< (double)cnt / kLoops * 100 << "%, expect 0.1%" << std::endl;
}
ASSERT_EQ(FaultInjectionFactor::get(), 1);
{
size_t cnt = 0;
FAULT_INJECTION_SET_FACTOR(100); // should be ignored
for (size_t i = 0; i < kLoops; i++) {
if (FAULT_INJECTION_WITH_FACTOR(5)) {
cnt++;
}
}
ASSERT_NE(cnt, 0);
std::cout << "Inject with factor 5, inject " << cnt << " faults during " << kLoops << " loops, "
<< (double)cnt / kLoops * 100 << "%, expect 0.2%" << std::endl;
}
{
FAULT_INJECTION_SET(10, 10);
size_t cnt = 0;
for (size_t i = 0; i < kLoops; i++) {
if (FAULT_INJECTION()) {
cnt++;
}
}
ASSERT_EQ(cnt, 10);
}
}
TEST(TestFaultInjection, Coroutine) {
ASSERT_EQ(FaultInjection::get(), nullptr);
folly::coro::blockingWait([]() -> folly::coro::Task<void> {
folly::RequestContext::create();
FAULT_INJECTION_SET(10, -1);
co_return;
}());
ASSERT_EQ(FaultInjection::get(), nullptr);
FAULT_INJECTION_SET(10, -1);
auto fi = FaultInjection::get();
ASSERT_NE(fi, nullptr);
folly::coro::blockingWait([]() -> folly::coro::Task<void> {
folly::RequestContext::create();
FAULT_INJECTION_SET(10, -1);
co_return;
}());
ASSERT_EQ(FaultInjection::get(), fi);
{
folly::CPUThreadPoolExecutor executor(4);
folly::coro::co_invoke([fi]() -> folly::coro::Task<void> {
CO_ASSERT_EQ(FaultInjection::get(), fi);
co_return;
})
.scheduleOn(&executor)
.start()
.wait();
executor.join();
}
{
FAULT_INJECTION_SET(10, 1000);
folly::CPUThreadPoolExecutor executor(4);
std::vector<folly::SemiFuture<size_t>> futures;
for (int i = 0; i < 10; i++) {
auto future = folly::coro::co_invoke([&]() -> folly::coro::Task<size_t> {
size_t cnts = 0;
for (int i = 0; i < 100000; i++) {
if (FAULT_INJECTION()) {
cnts++;
}
if (folly::Random::oneIn(100)) co_await folly::coro::co_reschedule_on_current_executor_t();
}
co_return cnts;
})
.scheduleOn(&executor)
.start();
futures.push_back(std::move(future));
}
auto results = folly::collectAll(futures.begin(), futures.end()).wait().result().value();
size_t cnts = 0;
for (auto &result : results) {
cnts += result.value();
}
executor.join();
ASSERT_EQ(cnts, 1000);
}
}
TEST(TestFaultInjection, Coroutine1) {
folly::CPUThreadPoolExecutor exec(1);
auto coro1 = [](FaultInjection *ptr) -> CoTask<void> {
CO_ASSERT_EQ(FaultInjection::get(), ptr);
co_return;
};
auto coro2 = [&]() -> CoTask<void> {
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
FAULT_INJECTION_SET(10, 1);
co_await coro1(FaultInjection::get()).scheduleOn(&exec).start();
co_return;
};
coro2().scheduleOn(&exec).start().wait();
sleep(1);
}
TEST(TestFaultInjection, Coroutine2) {
folly::CPUThreadPoolExecutor exec(1);
auto coro1 = []() -> CoTask<void> {
CO_ASSERT_EQ(folly::RequestContext::try_get(), nullptr);
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
folly::RequestContext::create();
FAULT_INJECTION_SET(10, 1);
CO_ASSERT_NE(folly::RequestContext::try_get(), nullptr);
CO_ASSERT_NE(FaultInjection::get(), nullptr);
co_return;
};
auto coro2 = [&]() -> CoTask<void> {
CO_ASSERT_EQ(folly::RequestContext::try_get(), nullptr);
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
co_await coro1().scheduleOn(&exec).start();
CO_ASSERT_EQ(folly::RequestContext::try_get(), nullptr);
CO_ASSERT_EQ(FaultInjection::get(), nullptr);
co_return;
};
coro2().scheduleOn(&exec).start().wait();
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,24 @@
#include <gtest/gtest.h>
#include "common/utils/FdWrapper.h"
namespace hf3fs::test {
namespace {
TEST(TestFdWrapper, Normal) {
int p[2];
ASSERT_EQ(::pipe(p), 0);
FdWrapper fd;
ASSERT_EQ(fd, -1);
fd = p[0];
fd = FdWrapper{p[1]}; // p[0] is closed.
ASSERT_EQ(fd, p[1]);
fd = FdWrapper{}; // p[1] is closed.
ASSERT_EQ(fd, -1);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,14 @@
#include <fmt/chrono.h>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <gtest/gtest.h>
namespace hf3fs::tests {
namespace {
TEST(Fmt, testFormat) {
ASSERT_EQ(fmt::format("Hello {}", "world"), "Hello world");
ASSERT_EQ(fmt::format("{}", std::vector<std::string>{"\naan"}), "[\"\\naan\"]");
ASSERT_EQ(fmt::format("{:%S}", std::chrono::milliseconds(1234)), "01.234");
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,27 @@
#include <folly/hash/Checksum.h>
#include <folly/logging/xlog.h>
#include "tests/GtestHelpers.h"
namespace hf3fs::tests {
namespace {
TEST(Folly, CRC32C) {
std::string a = "hello";
auto crc1 = folly::crc32c(reinterpret_cast<const uint8_t *>(a.data()), a.size(), 0);
std::string b = "world";
auto crc2 = folly::crc32c(reinterpret_cast<const uint8_t *>(b.data()), b.size(), 0);
auto x = folly::crc32c_combine(crc1, crc2, b.size());
auto y = folly::crc32c(reinterpret_cast<const uint8_t *>(b.data()), b.size(), crc1);
ASSERT_EQ(x, y);
auto crc32c1 = ~0x14298C12; // 1MB zero.
auto crc32c2 = ~0x527D5351; // one zero.
auto out = folly::crc32c_combine(~crc32c1, crc32c2, 1);
XLOGF(INFO, "{:08X} {:08X}", out, ~out);
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,16 @@
#include <cstdint>
#include <folly/hash/Hash.h>
#include "common/utils/RobinHoodUtils.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
TEST(HashCombine, testUint64) {
for (uint64_t x = 0; x < 64; ++x) {
auto h = folly::hash::hash_combine_generic(RobinHoodHasher{}, x);
fmt::print("x = {} h = {} h % 256 = {}\n", x, h, h % 256);
}
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,175 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <fmt/core.h>
#include <folly/Random.h>
#include <folly/Synchronized.h>
#include <folly/Unit.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/CurrentExecutor.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/futures/Future.h>
#include <gtest/gtest.h>
#include <memory>
#include <mutex>
#include <vector>
#include "common/kv/mem/MemKVEngine.h"
#include "common/utils/Coroutine.h"
#include "common/utils/FaultInjection.h"
#include "common/utils/IdAllocator.h"
#include "fdb/FDBRetryStrategy.h"
#include "fdb/FDBTransaction.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
kv::FDBRetryStrategy retryStrategy() { return kv::FDBRetryStrategy({.retryMaybeCommitted = true}); }
TEST(TestIdAllocator, Basic) {
folly::coro::blockingWait([&]() -> CoTask<void> {
kv::MemKVEngine engine;
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 1);
for (size_t i = 1; i <= 1000; i++) {
auto result = co_await allocator.allocate();
CO_ASSERT_OK(result);
CO_ASSERT_EQ(result.value(), i);
}
}());
}
TEST(TestIdAllocator, MultiThreads) {
kv::MemKVEngine engine;
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 1);
std::atomic<size_t> failed = 0;
folly::Synchronized<std::vector<uint64_t>, std::mutex> allocated;
std::vector<folly::SemiFuture<folly::Unit>> futures;
folly::CPUThreadPoolExecutor exec(8);
for (size_t i = 0; i < 128; i++) {
auto task = [&]() -> CoTask<void> {
for (size_t i = 1; i <= 1000; i++) {
auto result = co_await allocator.allocate();
if (result.hasError()) {
CO_ASSERT_ERROR(result, TransactionCode::kConflict);
failed++;
} else {
allocated.lock()->push_back(result.value());
}
co_await folly::coro::co_reschedule_on_current_executor;
}
co_return;
};
futures.push_back(folly::coro::co_invoke(task).scheduleOn(&exec).start());
}
for (auto &f : futures) {
f.wait();
}
auto lock = allocated.lock();
std::sort(lock->begin(), lock->end());
fmt::print("success {}, failed {}, first {}, last {}\n",
lock->size(),
failed.load(),
*lock->begin(),
*lock->rbegin());
for (size_t i = 1; i < lock->size(); i++) {
ASSERT_GT(lock->at(i), lock->at(i - 1));
}
}
TEST(TestIdAllocator, Sharded) {
folly::coro::blockingWait([&]() -> CoTask<void> {
kv::MemKVEngine engine;
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 8);
std::vector<uint64_t> allocated;
for (size_t i = 1; i <= 1000; i++) {
auto result = co_await allocator.allocate();
CO_ASSERT_OK(result);
allocated.push_back(result.value());
}
std::sort(allocated.begin(), allocated.end());
for (size_t i = 1; i < allocated.size(); i++) {
CO_ASSERT_GT(allocated.at(i), allocated.at(i - 1));
}
auto status = co_await allocator.status();
CO_ASSERT_OK(status);
std::sort(status->begin(), status->end());
fmt::print("shard usage: min {}, max {}\n", *status->begin(), *status->rbegin());
}());
}
TEST(TestIdAllocator, ShardedMultiThreads) {
kv::MemKVEngine engine;
std::vector<std::unique_ptr<IdAllocator<kv::FDBRetryStrategy>>> vec;
for (int i = 0; i < 8; i++) {
vec.emplace_back(std::make_unique<IdAllocator<kv::FDBRetryStrategy>>(engine, retryStrategy(), "test", 32));
}
std::atomic<size_t> failed = 0;
folly::Synchronized<std::vector<uint64_t>, std::mutex> allocated;
std::vector<folly::SemiFuture<folly::Unit>> futures;
folly::CPUThreadPoolExecutor exec(8);
for (size_t i = 0; i < 128; i++) {
auto task = [&, &allocator = vec[i % vec.size()]]() -> CoTask<void> {
for (size_t i = 1; i <= 1000; i++) {
auto result = co_await allocator->allocate();
if (result.hasError()) {
CO_ASSERT_ERROR(result, TransactionCode::kConflict);
failed++;
} else {
allocated.lock()->push_back(result.value());
}
co_await folly::coro::co_reschedule_on_current_executor;
}
co_return;
};
futures.push_back(folly::coro::co_invoke(task).scheduleOn(&exec).start());
}
for (auto &f : futures) {
f.wait();
}
auto lock = allocated.lock();
std::sort(lock->begin(), lock->end());
fmt::print("success {}, failed {}, min {}, max {}\n", lock->size(), failed.load(), *lock->begin(), *lock->rbegin());
for (size_t i = 1; i < lock->size(); i++) {
ASSERT_GT(lock->at(i), lock->at(i - 1));
}
auto status = vec[0]->status().scheduleOn(&exec).start().wait().value();
ASSERT_OK(status);
std::sort(status->begin(), status->end());
fmt::print("shard usage: min {}, max {}\n", *status->begin(), *status->rbegin());
}
TEST(TestIdAllocator, FaultInjection) {
folly::coro::blockingWait([&]() -> CoTask<void> {
FAULT_INJECTION_SET(2, -1);
kv::MemKVEngine engine;
IdAllocator<kv::FDBRetryStrategy> allocator(engine, retryStrategy(), "test", 8);
std::set<uint64_t> set;
size_t fail = 0;
for (size_t i = 1; i <= 1000; i++) {
auto result = co_await allocator.allocate();
if (result.hasValue()) {
CO_ASSERT_FALSE(set.contains(result.value()));
set.insert(result.value());
} else {
fail++;
}
}
std::cout << "success " << set.size() << ", fail " << fail << std::endl;
}());
}
} // namespace hf3fs::test

View File

@@ -0,0 +1,23 @@
#include <fmt/format.h>
#include <limits>
#include <set>
#include "common/utils/Int128.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
TEST(TestInt128, Normal) {
auto value = std::numeric_limits<uint128_t>::max();
ASSERT_EQ(fmt::format("{}", value), "340282366920938463463374607431768211455");
std::set<uint128_t> set;
set.insert(value);
ASSERT_EQ(set.size(), 1ul);
ASSERT_TRUE(set.count(value));
ASSERT_FALSE(set.count(0));
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,85 @@
#include <fcntl.h>
#include <liburing.h>
#include <liburing/io_uring.h>
#include "common/utils/FdWrapper.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
TEST(TestLibUring, Normal) {
constexpr auto N = 1024;
FdWrapper fd{::open("/dev/zero", O_RDONLY)};
ASSERT_TRUE(fd);
struct io_uring ring;
ASSERT_EQ(io_uring_queue_init(N, &ring, 0), 0);
std::string buf(N, 'A');
struct iovec iovec;
iovec.iov_base = buf.data();
iovec.iov_len = N;
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
ASSERT_NE(sqe, nullptr);
io_uring_prep_readv(sqe, fd, &iovec, 1, 0);
io_uring_sqe_set_data(sqe, nullptr);
ASSERT_EQ(io_uring_submit(&ring), 1);
struct io_uring_cqe *cqe;
ASSERT_EQ(::io_uring_wait_cqe(&ring, &cqe), 0);
ASSERT_EQ(cqe->res, N);
::io_uring_cqe_seen(&ring, cqe);
ASSERT_EQ(buf, std::string(N, '\0'));
::io_uring_queue_exit(&ring);
}
TEST(TestLibUring, SQPolling) {
if (getuid()) {
// skip if non-root.
GTEST_SKIP_("Skip SQPolling if non-root");
}
constexpr auto N = 256;
FdWrapper fd{::open("/dev/zero", O_RDONLY)};
ASSERT_TRUE(fd);
struct io_uring ring {};
struct io_uring_params params {};
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_idle = 50;
ASSERT_EQ(io_uring_queue_init_params(N, &ring, &params), 0);
int files[1] = {fd};
ASSERT_EQ(io_uring_register_files(&ring, files, 1), 0);
std::string buf(N, 'A');
struct iovec iovec;
iovec.iov_base = buf.data();
iovec.iov_len = N;
ASSERT_EQ(io_uring_register_buffers(&ring, &iovec, 1), 0);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
ASSERT_NE(sqe, nullptr);
io_uring_prep_read_fixed(sqe, 0, iovec.iov_base, N, 0, 0);
sqe->flags |= IOSQE_FIXED_FILE;
io_uring_sqe_set_data(sqe, nullptr);
ASSERT_EQ(io_uring_submit(&ring), 1);
struct io_uring_cqe *cqe;
ASSERT_EQ(::io_uring_wait_cqe(&ring, &cqe), 0);
ASSERT_EQ(cqe->res, N);
::io_uring_cqe_seen(&ring, cqe);
ASSERT_EQ(buf, std::string(N, '\0'));
::io_uring_queue_exit(&ring);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,136 @@
#include <chrono>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/logging/xlog.h>
#include <mutex>
#include <thread>
#include "common/utils/LockManager.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
using namespace std::chrono_literals;
TEST(TestLockManager, Normal) {
UniqueLockManager lockManager;
auto lock1 = lockManager.lock(123);
auto lock2 = lockManager.lock(std::string{"yes"});
auto lock3 = lockManager.try_lock(124);
ASSERT_TRUE(lock3.owns_lock());
auto lock4 = lockManager.try_lock(123);
ASSERT_FALSE(lock4.owns_lock());
lock1.unlock();
auto lock5 = lockManager.try_lock(123);
ASSERT_TRUE(lock5.owns_lock());
auto lock6 = lockManager.try_lock(std::string{"yes"});
ASSERT_FALSE(lock6.owns_lock());
}
TEST(TestLockManager, SharedLock) {
SharedLockManager lockManager;
auto lock = lockManager.lock_shared(123);
std::jthread([&] { auto anotherLock = lockManager.lock_shared(123); });
}
TEST(TestLockManager, FairSharedLock) {
SharedLockManager lockManager;
std::jthread read1([&] {
// 0ms
auto start = std::chrono::steady_clock::now();
auto lock = lockManager.lock_shared(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
XLOGF(INFO, "read1 lock wait {}ms", elapsed.count());
std::this_thread::sleep_for(100ms);
// 100ms
std::this_thread::sleep_for(100ms);
// 200ms
std::this_thread::sleep_for(100ms);
// 300ms
lock.unlock();
std::this_thread::sleep_for(100ms);
// 400ms
lock.lock();
std::this_thread::sleep_for(100ms);
// 500ms
std::this_thread::sleep_for(100ms);
// 600ms
});
std::jthread read2([&] {
// 0ms
std::this_thread::sleep_for(100ms);
// 100ms
std::this_thread::sleep_for(100ms);
// 200ms
auto start = std::chrono::steady_clock::now();
auto lock = lockManager.lock_shared(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
XLOGF(INFO, "read2 lock wait {}ms", elapsed.count());
std::this_thread::sleep_for(100ms);
// 300ms
std::this_thread::sleep_for(100ms);
// 400ms
std::this_thread::sleep_for(100ms);
// 500ms
lock.unlock();
});
std::jthread write([&] {
// 0ms
std::this_thread::sleep_for(100ms);
// 100ms
auto start = std::chrono::steady_clock::now();
auto lock = lockManager.lock(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
XLOGF(INFO, "write lock wait {}ms", elapsed.count());
ASSERT_NEAR(elapsed.count(), 200, 50);
});
}
TEST(TestLockManager, CoMutex) {
CoUniqueLockManager lockManager;
folly::coro::blockingWait([&]() -> folly::coro::Task<void> {
auto lock1 = co_await lockManager.co_scoped_lock(123);
auto lock2 = co_await lockManager.co_scoped_lock(std::string{"yes"});
auto lock3 = lockManager.try_lock(124);
CO_ASSERT_TRUE(lock3.owns_lock());
auto lock4 = lockManager.try_lock(123);
CO_ASSERT_FALSE(lock4.owns_lock());
lock1.unlock();
auto lock5 = lockManager.try_lock(123);
CO_ASSERT_TRUE(lock5.owns_lock());
auto lock6 = lockManager.try_lock(std::string{"yes"});
CO_ASSERT_FALSE(lock6.owns_lock());
}());
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,82 @@
#include <gtest/gtest.h>
#include "common/utils/LruCache.h"
namespace hf3fs::test {
namespace {
TEST(TestLruCache, Normal) {
LruCache<int, int> cache(4);
ASSERT_TRUE(cache.empty());
ASSERT_EQ(cache.size(), 0);
ASSERT_EQ(cache.begin(), cache.end());
cache[1] = 1;
cache[2] = 2;
cache[3] = 3;
cache[4] = 4;
ASSERT_FALSE(cache.empty());
ASSERT_EQ(cache.size(), 4);
ASSERT_NE(cache.begin(), cache.end());
auto it = cache.begin();
ASSERT_EQ(it->second, 4);
++it;
ASSERT_EQ(it->second, 3);
++it;
ASSERT_EQ(it->second, 2);
++it;
ASSERT_EQ(it->second, 1);
++it;
ASSERT_EQ(it, cache.end());
cache[1] = 1;
ASSERT_EQ(cache.front().second, 1);
ASSERT_EQ(cache.back().second, 2);
ASSERT_TRUE(cache.emplace(5, 5).second);
ASSERT_EQ(cache.front().second, 5);
ASSERT_EQ(cache.back().second, 3);
ASSERT_EQ(cache.size(), 4);
cache.promote(cache.find(1));
ASSERT_EQ(cache.front().second, 1);
ASSERT_EQ(cache.back().second, 3);
cache.obsolete(cache.find(1));
ASSERT_EQ(cache.front().second, 5);
ASSERT_EQ(cache.back().second, 1);
it = cache.erase(5);
ASSERT_EQ(it, cache.begin());
it = cache.erase(cache.find(1));
ASSERT_EQ(it, cache.end());
ASSERT_EQ(cache.size(), 2);
auto idx = 0;
for (auto &pair : cache) {
ASSERT_EQ(pair.first, 4 - idx++);
}
}
TEST(TestLruCache, ManuallyRemove) {
constexpr auto N = 4;
constexpr auto M = 8;
LruCache<int, int> cache(N, false);
for (auto i = 0; i < M; ++i) {
cache[i] = i;
}
ASSERT_EQ(cache.size(), M);
cache.evictObsoletedIf([](int, int value) { return value < 2; });
ASSERT_EQ(cache.size(), 6);
ASSERT_EQ(cache.back().second, 2);
cache.evictObsoleted();
ASSERT_EQ(cache.size(), N);
ASSERT_EQ(cache.back().second, 4);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,31 @@
#include <gtest/gtest.h>
#include <memory>
#include "common/utils/MPSCQueue.h"
namespace hf3fs::test {
namespace {
TEST(TestMPSCQueue, Normal) {
constexpr auto N = 4096u;
MPSCQueue<size_t> que(N);
for (auto i = 0u; i < N; ++i) {
auto item = std::make_unique<size_t>(i);
ASSERT_TRUE(que.push(&item));
ASSERT_TRUE(item == nullptr);
}
auto more = std::make_unique<size_t>(0);
ASSERT_EQ(que.push(&more).code(), StatusCode::kQueueFull);
ASSERT_TRUE(more != nullptr);
for (auto i = 0u; i < N; ++i) {
std::unique_ptr<size_t> item;
ASSERT_TRUE(que.pop(&item));
ASSERT_TRUE(item != nullptr);
}
ASSERT_EQ(que.pop(&more).code(), StatusCode::kQueueEmpty);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,100 @@
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Sleep.h>
#include <gtest/gtest.h>
#include <thread>
#include "common/utils/Coroutine.h"
#include "common/utils/Semaphore.h"
#include "common/utils/SemaphoreGuard.h"
#include "common/utils/Size.h"
#include "memory/common/GlobalMemoryAllocator.h"
namespace hf3fs::test {
namespace {
TEST(TestMemoryAllocator, Basic) {
size_t totalSize = 512_MB;
size_t blockSize = 16_KB;
std::vector<void *> memBlocks;
for (size_t allocatedSize = 0; allocatedSize < totalSize; allocatedSize += blockSize) {
auto mem = memory::allocate(blockSize);
memBlocks.push_back(mem);
}
char logbuf[512] = {0};
memory::logstatus(logbuf, sizeof(logbuf));
XLOGF(INFO, "Memory allocator log: {}", logbuf);
for (auto mem : memBlocks) {
memory::deallocate(mem);
}
memory::logstatus(logbuf, sizeof(logbuf));
XLOGF(INFO, "Memory allocator log: {}", logbuf);
}
TEST(TestMemoryAllocator, AlignedAlloc) {
static size_t kPageSize = sysconf(_SC_PAGESIZE);
size_t totalSize = 512_MB;
size_t blockSize = 100_KB;
std::vector<void *> memBlocks;
for (size_t allocatedSize = 0; allocatedSize < totalSize; allocatedSize += blockSize) {
auto mem = memory::memalign(kPageSize, blockSize);
memBlocks.push_back(mem);
}
char logbuf[512] = {0};
memory::logstatus(logbuf, sizeof(logbuf));
XLOGF(INFO, "Memory allocator log: {}", logbuf);
for (auto mem : memBlocks) {
memory::deallocate(mem);
}
memory::logstatus(logbuf, sizeof(logbuf));
XLOGF(INFO, "Memory allocator log: {}", logbuf);
}
/* Set the following environment variables before run the profiling test
export JE_MALLOC_CONF="prof:true,prof_active:false"
export MEMORY_ALLOCATOR_LIB_PATH=build/src/memory/jemalloc/libjemalloc_wrapper.so
*/
TEST(TestMemoryAllocator, Profiling) {
size_t totalSize = 512_MB;
size_t blockSize = 16_KB;
std::vector<void *> memBlocks;
auto enabled = memory::profiling(true /*active*/, "prof");
// skip this test if profiling is not supported
if (!enabled) return;
for (size_t allocatedSize = 0; allocatedSize < totalSize; allocatedSize += blockSize) {
auto mem = memory::allocate(blockSize);
memBlocks.push_back(mem);
}
// re-enable to dump profiling
ASSERT_TRUE(memory::profiling(true /*active*/, nullptr));
// disable and dump profiling
ASSERT_TRUE(memory::profiling(false /*active*/, "prof"));
// enable profiling
enabled = memory::profiling(true /*active*/, "prof");
for (auto mem : memBlocks) {
memory::deallocate(mem);
}
// disable and dump profiling
ASSERT_TRUE(memory::profiling(false /*active*/, nullptr));
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,71 @@
#include <gtest/gtest.h>
#include "common/utils/ObjectPool.h"
namespace hf3fs::test {
namespace {
TEST(TestObjectPool, Normal) {
constexpr auto X = 123;
static size_t destructTimes = 0;
struct A {
~A() { ++destructTimes; }
int x = X;
};
A *rawPtr = nullptr;
{
auto a = ObjectPool<A>::get();
ASSERT_NE(a.get(), nullptr);
ASSERT_EQ(a->x, X);
a->x = 0;
rawPtr = a.get();
ASSERT_EQ(destructTimes, 0);
a.reset();
ASSERT_EQ(destructTimes, 1);
}
ASSERT_EQ(destructTimes, 1);
ASSERT_EQ(ObjectPool<A>::get().get(), rawPtr);
ASSERT_EQ(ObjectPool<A>::get()->x, X);
}
TEST(TestObjectPool, Allocate) {
constexpr auto N = 1000000;
static size_t constructTimes = 0;
static size_t destructTimes = 0;
struct A {
A() { ++constructTimes; }
~A() { ++destructTimes; }
};
{
std::vector<ObjectPool<A>::Ptr> vec{N};
for (auto &item : vec) {
item = ObjectPool<A>::get();
}
ASSERT_EQ(constructTimes, N);
ASSERT_EQ(destructTimes, 0);
}
ASSERT_EQ(destructTimes, N);
}
TEST(TestObjectPool, AllocateAndRelease) {
constexpr auto N = 1000000;
static size_t constructTimes = 0;
static size_t destructTimes = 0;
struct A {
A() { ++constructTimes; }
~A() { ++destructTimes; }
};
for (auto i = 0; i < N; ++i) {
ObjectPool<A>::get();
}
ASSERT_EQ(constructTimes, N);
ASSERT_EQ(destructTimes, N);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,47 @@
#include <chrono>
#include <folly/Executor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <gtest/gtest.h>
#include <thread>
#include "common/utils/Coroutine.h"
#include "common/utils/PriorityUnboundedQueue.h"
namespace hf3fs::test {
namespace {
TEST(TestPriorityBoundedQueue, Normal) {
PriorityUnboundedQueue<int> queue(3);
ASSERT_TRUE(queue.size() == 0);
queue.addWithPriority(1, folly::Executor::LO_PRI);
queue.addWithPriority(2, folly::Executor::HI_PRI);
queue.addWithPriority(3, folly::Executor::MID_PRI);
queue.addWithPriority(4, folly::Executor::HI_PRI);
ASSERT_EQ(queue.dequeue(), 2);
ASSERT_EQ(queue.dequeue(), 4);
ASSERT_EQ(queue.dequeue(), 3);
ASSERT_EQ(queue.dequeue(), 1);
}
TEST(TestPriorityBoundedQueue, Many) {
PriorityUnboundedQueue<int> queue(3);
ASSERT_TRUE(queue.size() == 0);
std::jthread a([&]() {
for (size_t i = 0; i < 2000000; i++) {
ASSERT_EQ(queue.dequeue(), 1);
}
});
std::jthread b([&]() {
for (size_t i = 0; i < 1000000; i++) {
queue.addWithPriority(1, folly::Executor::HI_PRI);
queue.addWithPriority(1, folly::Executor::LO_PRI);
}
});
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,115 @@
#include <chrono>
#include <folly/logging/xlog.h>
#include <gtest/gtest.h>
#include <mutex>
#include <thread>
#include "common/utils/ReentrantLockManager.h"
namespace hf3fs::test {
namespace {
using namespace std::chrono_literals;
TEST(TestReentrantLockManager, Normal) {
ReentrantLockManager lockManager;
lockManager.init();
auto lock1 = lockManager.writeLock(123);
auto lock2 = lockManager.writeLock(456);
auto lock3 = lockManager.tryWriteLock(124);
ASSERT_TRUE(lock3.writeLocked());
auto lock4 = lockManager.tryWriteLock(123);
ASSERT_FALSE(lock4.writeLocked());
lock1.unlock();
auto lock5 = lockManager.tryWriteLock(123);
ASSERT_TRUE(lock5.writeLocked());
auto lock6 = lockManager.tryWriteLock(456);
ASSERT_FALSE(lock6.writeLocked());
}
TEST(TestReentrantLockManager, SharedLock) {
ReentrantLockManager lockManager;
lockManager.init();
auto lock = lockManager.readLock(123);
std::jthread([&] { auto anotherLock = lockManager.readLock(123); });
}
TEST(TestReentrantLockManager, FairSharedLock) {
ReentrantLockManager lockManager;
lockManager.init();
std::jthread read1([&] {
// 0ms
auto start = std::chrono::steady_clock::now();
auto lock = lockManager.readLock(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
XLOGF(INFO, "read1 lock wait {}ms", elapsed.count());
std::this_thread::sleep_for(100ms);
// 100ms
std::this_thread::sleep_for(100ms);
// 200ms
std::this_thread::sleep_for(100ms);
// 300ms
lock.unlock();
std::this_thread::sleep_for(100ms);
// 400ms
lock = lockManager.readLock(1);
std::this_thread::sleep_for(100ms);
// 500ms
std::this_thread::sleep_for(100ms);
// 600ms
});
std::jthread read2([&] {
// 0ms
std::this_thread::sleep_for(100ms);
// 100ms
std::this_thread::sleep_for(100ms);
// 200ms
auto start = std::chrono::steady_clock::now();
auto lock = lockManager.readLock(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
XLOGF(INFO, "read2 lock wait {}ms", elapsed.count());
std::this_thread::sleep_for(100ms);
// 300ms
std::this_thread::sleep_for(100ms);
// 400ms
std::this_thread::sleep_for(100ms);
// 500ms
lock.unlock();
});
std::jthread write([&] {
// 0ms
std::this_thread::sleep_for(100ms);
// 100ms
auto start = std::chrono::steady_clock::now();
auto lock = lockManager.writeLock(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start);
XLOGF(INFO, "write lock wait {}ms", elapsed.count());
ASSERT_NEAR(elapsed.count(), 200, 50);
});
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,27 @@
#include "common/app/AppInfo.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
TEST(TestReleaseVersion, testFormat) {
auto rv = flat::ReleaseVersion::fromVersionInfoV0();
ASSERT_EQ(fmt::format("Version: v{}", rv), VersionInfo::fullV0());
rv = flat::ReleaseVersion::fromVersionInfo();
ASSERT_EQ(fmt::format("Version: v{}", rv), VersionInfo::full());
}
TEST(TestReleaseVersion, testDeserializeFromV0) {
auto rv = flat::ReleaseVersion::fromV0(0, 1, 4, 0xabcdef12, 1688016296, 54321);
auto str = serde::serialize(rv);
auto unpackRes = flat::ReleaseVersion::unpackFrom(str);
ASSERT_TRUE(unpackRes);
auto newRv = *unpackRes;
ASSERT_EQ(newRv.toString(), "0.1.4-54321-20230629-abcdef12");
auto rv2 = flat::ReleaseVersion::fromVersionInfo();
ASSERT_TRUE(rv2 > rv);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,187 @@
#include <cstdlib>
#include "common/utils/RenderConfig.h"
#include "common/utils/Toml.hpp"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
String toPrettyToml(const String &s) {
auto t = toml::parse(s);
std::stringstream ss;
ss << toml::toml_formatter(t, toml::toml_formatter::default_flags & ~toml::format_flags::indentation);
return ss.str();
}
TEST(RenderConfig, testNormalV0) {
flat::AppInfo app;
app.nodeId = flat::NodeId(2);
app.hostname = "hostname";
app.pid = 100;
app.tags = {flat::TagPair("k1", "v1"), flat::TagPair("k2"), flat::TagPair("k3", "v3")};
app.releaseVersion = flat::ReleaseVersion::fromV0(0, 1, 2, 0, 0, 100);
::setenv("TEST_RENDER_CONFIG", "abc", 1);
String rawConfig = R"(
[server]
{% if app.nodeId in [1, 3, 5] %}
queue_size = 10
{% elif hasTagValue(app, "k1", "v1") %}
queue_size = 20
{% else %}
queue_size = 30
{% endif %}
{% if app.hostname is startsWith("host") %}
zzz = 1
{% else %}
zzz = 0
{% endif %}
xxx = {{ "\"abc\"" if app is hasTag("k2") else "\"def\""}}
port0 = {{ 10003 % 2 }}
port1 = {{ 10007 % 2 }}
version_less_than = {{ relaseVersionCompare(app, "<", "0.1.2") }}
version_less_than_or_equal_to = {{ relaseVersionCompare(app, "<=", "0.1.2-100") }}
version_equal_to = {{ relaseVersionCompare(app, "==", "0.1.2") }}
version_greater_than = {{ releaseVersionCompare(app, ">", "0.1.2-100") }}
version_greater_than_or_equal_to = {{ releaseVersionCompare(app, ">=", "0.1.2") }}
version_not_equal_to = {{ releaseVersionCompare(app, "!=", "0.1.2-100") }}
yyy = "{{ env.TEST_RENDER_CONFIG }}"
log_level = '{{ env.HF3FS_LOG_LEVEL if env.HF3FS_LOG_LEVEL else "INFO" }}'
# TODO: implement startswith
{% if app.hostname == "hostname" %}
enable_fast_mode = true
{% endif %})";
auto res = renderConfig(rawConfig, &app);
ASSERT_OK(res);
auto resConfig = toPrettyToml(res.value());
auto expected = toPrettyToml(R"(
[server]
xxx = 'abc'
yyy = 'abc'
zzz = 1
log_level = 'INFO'
version_less_than = false
version_less_than_or_equal_to = true
version_equal_to = true
version_greater_than_or_equal_to = true
version_greater_than = false
version_not_equal_to = false
port0 = 1
port1 = 1
queue_size = 20 # hit second case
enable_fast_mode = true)");
ASSERT_EQ(resConfig, expected);
}
TEST(RenderConfig, testTemplateNotParsed) {
flat::AppInfo app;
app.nodeId = flat::NodeId(2);
app.hostname = "hostname";
app.pid = 100;
app.tags = {flat::TagPair("k1", "v1"), flat::TagPair("k2"), flat::TagPair("k3", "v3")};
app.releaseVersion = flat::ReleaseVersion::fromV0(0, 1, 2, 0, 0, 0);
::setenv("TEST_RENDER_CONFIG", "abc", 1);
String rawConfig = R"(
[server]
{% if app.nodeId in (1, 3, 5) %}
queue_size = 10
{% elif app.hostname not in ('hostname') %}
queue_size = 10
{% elif hasTagValue(app, "k1", "v1") %}
queue_size = 20
{% else %}
queue_size = 30
{% endif %}
xxx = {{ "\"abc\"" if app is hasTag("k2") else "\"def\""}}
{% if app.releaseVersion.major == 0 and app.releaseVersion.minor == 1 and app.releaseVersion.patch == 0 %}
yyy = 1
{% endif %}
version_less_than = {{ relaseVersionCompare(app, "<", "0.1.2") }}
version_less_than_or_equal_to = {{ relaseVersionCompare(app, "<=", "0.1.2") }}
version_equal_to = {{ relaseVersionCompare(app, "==", "0.1.2") }}
version_greater_than = {{ relaseVersionCompare(app, ">", "0.1.2") }}
version_greater_than_or_equal_to = {{ relaseVersionCompare(app, ">=", "0.1.2") }}
version_not_equal_to = {{ relaseVersionCompare(app, "!=", "0.1.2") }}
yyy = "{{ env.TEST_RENDER_CONFIG }}"
log_level = '{{ env.HF3FS_LOG_LEVEL if env.HF3FS_LOG_LEVEL else "INFO" }}'
# TODO: implement startswith
{% if app.hostname == "hostname" %}
enable_fast_mode = true
{% endif %})";
auto res = renderConfig(rawConfig, &app);
ASSERT_ERROR(res, StatusCode::kInvalidConfig);
ASSERT_TRUE(res.error().message().find("app.hostname not in") != std::string_view::npos);
}
#define ASSERT_REDNER_EQ(raw, app, expected) \
do { \
auto &&_raw = (raw); \
auto &&_app = (app); \
auto &&_expected = (expected); \
auto _res = renderConfig(_raw, _app); \
ASSERT_OK(_res); \
auto _resConfig = toPrettyToml(_res.value()); \
ASSERT_EQ(_resConfig, _expected); \
} while (0)
TEST(RenderConfig, testNormalV1) {
String config0 = R"(
x = {{ app.nodeId in [1, 2, 3, 4, 5] }}
{% set port = app.nodeId % 2 %}
{% if port == 0 %}
target_paths = ["/storage/data1/hf3fs", "/storage/data2/hf3fs", "/storage/data3/hf3fs", "/storage/data4/hf3fs", "/storage/data5/hf3fs", "/storage/data6/hf3fs", "/storage/data7/hf3fs", "/storage/data8/hf3fs"]
{% else %}
target_paths = ["/storage/data9/hf3fs", "/storage/data10/hf3fs", "/storage/data11/hf3fs", "/storage/data12/hf3fs", "/storage/data13/hf3fs", "/storage/data14/hf3fs", "/storage/data15/hf3fs", "/storage/data16/hf3fs"]
{% endif %}
)";
String config1 = R"(
x = {{ app.nodeId in range(1, 6) }}
target_paths = [ {% for i in range(8) %} "/storage/data{{i + 8 * (app.nodeId % 2) + 1}}/hf3fs", {% endfor %} ]
)";
flat::AppInfo app0;
app0.nodeId = flat::NodeId(0);
auto expected0 = toPrettyToml(R"(
x = false
target_paths = ["/storage/data1/hf3fs", "/storage/data2/hf3fs", "/storage/data3/hf3fs", "/storage/data4/hf3fs", "/storage/data5/hf3fs", "/storage/data6/hf3fs", "/storage/data7/hf3fs", "/storage/data8/hf3fs"]
)");
ASSERT_REDNER_EQ(config0, &app0, expected0);
ASSERT_REDNER_EQ(config1, &app0, expected0);
flat::AppInfo app1;
app1.nodeId = flat::NodeId(1);
auto expected1 = toPrettyToml(R"(
x = true
target_paths = ["/storage/data9/hf3fs", "/storage/data10/hf3fs", "/storage/data11/hf3fs", "/storage/data12/hf3fs", "/storage/data13/hf3fs", "/storage/data14/hf3fs", "/storage/data15/hf3fs", "/storage/data16/hf3fs"]
)");
ASSERT_REDNER_EQ(config0, &app1, expected1);
ASSERT_REDNER_EQ(config1, &app1, expected1);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,103 @@
#include <atomic>
#include <chrono>
#include <fmt/core.h>
#include <folly/Random.h>
#include <folly/ScopeGuard.h>
#include <folly/concurrency/UnboundedQueue.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/executors/IOThreadPoolExecutor.h>
#include <folly/experimental/coro/AsyncGenerator.h>
#include <folly/experimental/coro/Baton.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/BoundedQueue.h>
#include <folly/experimental/coro/CurrentExecutor.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Invoke.h>
#include <folly/experimental/coro/ScopeExit.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/experimental/coro/Task.h>
#include <folly/experimental/coro/UnboundedQueue.h>
#include <folly/fibers/Semaphore.h>
#include <folly/futures/Future.h>
#include <folly/io/async/Request.h>
#include <gtest/gtest.h>
#include <optional>
#include <type_traits>
#include <vector>
#include "common/utils/Coroutine.h"
namespace hf3fs::test {
namespace {
TEST(TestRequestContext, FibersBaton) {
folly::CPUThreadPoolExecutor exec(folly::Random::rand32(1) + 1);
folly::fibers::Baton baton;
auto producer = folly::coro::co_invoke([&]() -> CoTask<void> {
co_await folly::coro::sleep(std::chrono::seconds(1));
folly::RequestContext::create();
baton.post();
co_return;
})
.scheduleOn(&exec)
.start();
auto consumer = folly::coro::co_invoke([&]() -> CoTask<void> {
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
co_await baton;
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
})
.scheduleOn(&exec)
.start();
producer.wait();
consumer.wait();
}
TEST(TestRequestContext, CoroBaton) {
folly::CPUThreadPoolExecutor exec(folly::Random::rand32(1) + 1);
folly::coro::Baton baton;
auto producer = folly::coro::co_invoke([&]() -> CoTask<void> {
co_await folly::coro::sleep(std::chrono::seconds(1));
folly::RequestContext::create();
baton.post();
co_return;
})
.scheduleOn(&exec)
.start();
auto consumer = folly::coro::co_invoke([&]() -> CoTask<void> {
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
co_await baton;
EXPECT_EQ(folly::RequestContext::try_get(), nullptr);
})
.scheduleOn(&exec)
.start();
producer.wait();
consumer.wait();
}
TEST(TestRequestContext, Semaphore) {
folly::CPUThreadPoolExecutor exec(folly::Random::rand32(1, 8));
folly::fibers::Semaphore semaphore(folly::Random::rand32(1, 8));
auto worker = [&]() -> CoTask<void> {
for (size_t i = 0; i < folly::Random::rand32(1000, 2000); i++) {
std::optional<folly::ShallowCopyRequestContextScopeGuard> guard;
if (folly::Random::oneIn(2)) {
guard.emplace();
}
auto ctx = folly::RequestContext::try_get();
co_await semaphore.co_wait();
SCOPE_EXIT { semaphore.signal(); };
CO_ASSERT_EQ(ctx, folly::RequestContext::try_get());
}
};
std::vector<folly::SemiFuture<Void>> tasks;
for (size_t i = 0; i < 64; i++) {
tasks.push_back(folly::coro::co_invoke(worker).scheduleOn(&exec).start());
}
for (auto &task : tasks) {
task.wait();
}
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,50 @@
#include <gtest/gtest.h>
#include <scn/scn.h>
#include <scn/tuple_return.h>
namespace hf3fs::test {
namespace {
TEST(TestScnlib, ScanString) {
std::string word;
auto result = scn::scan("Hello world!", "{}", word);
ASSERT_EQ(word, "Hello");
ASSERT_EQ(result.range_as_string(), " world!");
}
TEST(TestScnlib, ScanMultipleValues) {
int i, j;
auto result = scn::scan("123 456 foo", "{} {}", i, j);
ASSERT_EQ(i, 123);
ASSERT_EQ(j, 456);
std::string str;
auto ret = scn::scan(result.range(), "{}", str);
ASSERT_EQ(static_cast<bool>(ret), true);
ASSERT_EQ(str, "foo");
}
TEST(TestScnlib, ScanTuple) {
{
auto [r, i] = scn::scan_tuple<int>("42", "{}");
ASSERT_EQ(static_cast<bool>(r), true);
ASSERT_EQ(i, 42);
}
{
auto [r, i, s] = scn::scan_tuple<int, std::string>("42 foo", "{} {}");
ASSERT_EQ(static_cast<bool>(r), true);
ASSERT_EQ(i, 42);
ASSERT_EQ(s, "foo");
}
}
TEST(TestScnlib, ErrorHandling) {
int i;
auto result = scn::scan("foo", "{}", i);
ASSERT_EQ(static_cast<bool>(result), false);
ASSERT_EQ(result.range_as_string(), "foo");
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,73 @@
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/GtestHelpers.h>
#include <folly/experimental/coro/Sleep.h>
#include <gtest/gtest.h>
#include <thread>
#include "common/utils/Coroutine.h"
#include "common/utils/Semaphore.h"
#include "common/utils/SemaphoreGuard.h"
namespace hf3fs::test {
namespace {
CoTask<size_t> runTask(Semaphore &sema, size_t numLoops) {
for (size_t i = 0; i < numLoops; i++) {
SemaphoreGuard lock(sema);
co_await lock.coWait();
co_await folly::coro::sleep(std::chrono::microseconds{0});
}
co_return numLoops;
}
std::vector<folly::SemiFuture<size_t>> startTasks(Semaphore &sema,
folly::CPUThreadPoolExecutor &threadPool,
size_t numTasks,
size_t numLoops) {
std::vector<folly::SemiFuture<size_t>> tasks;
for (size_t t = 0; t < numTasks; t++) {
tasks.push_back(runTask(sema, numLoops).scheduleOn(folly::Executor::getKeepAliveToken(threadPool)).start());
}
return tasks;
}
TEST(TestSemaphore, Basic) {
size_t numCpuCores = std::thread::hardware_concurrency();
size_t numTasks = numCpuCores * 2;
size_t numLoops = 100;
Semaphore sema(std::max(size_t(1), numCpuCores / 2));
folly::CPUThreadPoolExecutor threadPool(numCpuCores / 2);
std::vector<folly::SemiFuture<size_t>> tasks = startTasks(sema, threadPool, numTasks, numLoops);
auto results = folly::coro::blockingWait(folly::coro::collectAllRange(std::move(tasks)));
size_t sumLoops = 0;
for (auto res : results) sumLoops += res;
ASSERT_EQ(numTasks * numLoops, sumLoops);
}
TEST(TestSemaphore, ChangeUsableTokens) {
size_t numCpuCores = std::thread::hardware_concurrency();
size_t numTasks = numCpuCores * 2;
size_t numLoops = 100;
Semaphore sema(std::max(size_t(1), numCpuCores / 2));
folly::CPUThreadPoolExecutor threadPool(numCpuCores / 2);
std::vector<folly::SemiFuture<size_t>> tasks = startTasks(sema, threadPool, numTasks, numLoops);
sema.changeUsableTokens(numTasks);
auto results = folly::coro::blockingWait(folly::coro::collectAllRange(std::move(tasks)));
size_t sumLoops = 0;
for (auto res : results) sumLoops += res;
ASSERT_EQ(numTasks * numLoops, sumLoops);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,130 @@
#include <common/utils/SerDeser.h>
#include <gtest/gtest.h>
namespace hf3fs::tests {
namespace {
TEST(Serializer, testPutChar) {
String s;
Serializer ser(s);
ser.putChar('a');
ser.putChar('b');
ser.putChar('c');
ASSERT_EQ(s, "abc");
}
TEST(Serializer, testPutIntAsChar) {
String s;
Serializer ser(s);
ser.putIntAsChar<int16_t>('a');
ser.putIntAsChar<int32_t>('b');
ser.putIntAsChar<int64_t>('c');
ASSERT_EQ(s, "abc");
}
TEST(Serializer, testPutShortString) {
String s;
Serializer ser(s);
ser.putShortString("abc");
ASSERT_EQ(s.size(), 4);
ASSERT_EQ(s[0], 3);
ASSERT_EQ(s.substr(1), "abc");
}
TEST(Deserializer, testGetChar) {
String s("abc");
Deserializer des(s);
String res;
for (auto c = des.getChar(); c.hasValue(); c = des.getChar()) {
res.push_back(c.value());
}
ASSERT_TRUE(des.reachEnd());
ASSERT_TRUE(!des.getChar().hasValue());
ASSERT_EQ(res, "abc");
}
TEST(Deserializer, testGetIntFromChar) {
String s;
Serializer ser(s);
ser.putIntAsChar<int>(100);
ser.putIntAsChar<int16_t>(-50);
ser.putIntAsChar<uint32_t>(50);
Deserializer des(s);
auto c0 = des.getIntFromChar<int>();
ASSERT_TRUE(c0.hasValue());
ASSERT_EQ(c0.value(), 100);
auto c1 = des.getIntFromChar<int16_t>();
ASSERT_TRUE(c1.hasValue());
ASSERT_EQ(c1.value(), -50);
auto c2 = des.getIntFromChar<uint32_t>();
ASSERT_TRUE(c2.hasValue());
ASSERT_EQ(c2.value(), 50);
ASSERT_TRUE(des.reachEnd());
ASSERT_TRUE(!des.getIntFromChar<int8_t>().hasValue());
}
TEST(Deserializer, testGetShortString) {
String s;
Serializer ser(s);
ser.putShortString("abc");
Deserializer des(s);
ASSERT_EQ(des.getShortString().value(), "abc");
ASSERT_TRUE(des.reachEnd());
ASSERT_TRUE(!des.getShortString().hasValue());
}
TEST(Deserializer, testGet) {
struct T {
int x;
char y;
};
T t = {1, 2};
String s;
Serializer ser(s);
ser.put(t);
Deserializer des(s);
auto res = des.get<T>();
ASSERT_TRUE(res.hasValue());
ASSERT_TRUE(des.reachEnd());
ASSERT_EQ(res.value().x, 1);
ASSERT_EQ(res.value().y, 2);
ASSERT_TRUE(!des.get<T>().hasValue());
}
TEST(Deserializer, testGetRaw) {
String s = "abcdefg";
Deserializer des(s);
auto res = des.getRaw(1);
ASSERT_TRUE(res.hasValue());
ASSERT_EQ(res.value(), "a");
res = des.getRaw(2);
ASSERT_TRUE(res.hasValue());
ASSERT_EQ(res.value(), "bc");
res = des.getRaw(5);
ASSERT_TRUE(!res.hasValue());
res = des.getRaw(1);
ASSERT_TRUE(res.hasValue());
ASSERT_EQ(res.value(), "d");
res = des.getRawUntilEnd();
ASSERT_TRUE(res.hasValue());
ASSERT_EQ(res.value(), "efg");
res = des.getRawUntilEnd();
ASSERT_TRUE(res.hasValue());
ASSERT_EQ(res.value(), "");
ASSERT_TRUE(des.reachEnd());
ASSERT_TRUE(!des.getRaw(1).hasValue());
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,41 @@
#include <folly/Random.h>
#include <thread>
#include <vector>
#include "common/utils/RobinHood.h"
#include "common/utils/Shards.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
TEST(TestShards, Normal) {
Shards<std::unordered_map<uint32_t, uint32_t>, 64> shards;
constexpr auto N = 8;
constexpr auto M = 100000;
std::vector<std::thread> threads(N);
for (auto &thread : threads) {
thread = std::thread([&shards] {
for (auto i = 0; i < M; ++i) {
auto r = folly::Random::rand32();
shards.withLock([&](std::unordered_map<uint32_t, uint32_t> &map) { ++map[r]; }, r);
}
});
}
for (auto &thread : threads) {
thread.join();
}
uint64_t sum = 0;
shards.iterate([&sum](std::unordered_map<uint32_t, uint32_t> &map) {
for (auto &[k, v] : map) {
sum += v;
}
});
ASSERT_EQ(sum, M * N);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,121 @@
#include <folly/logging/xlog.h>
#include "common/utils/ConfigBase.h"
#include "common/utils/Size.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
static_assert(config::IsPrimitive<Size>, "size is not primitive");
class Config : public ConfigBase<Config> {
CONFIG_HOT_UPDATED_ITEM(size, 128_KB);
};
TEST(TestSize, Normal) {
Size config;
ASSERT_EQ(size_t(config), 0);
ASSERT_EQ(1024_B, 1_KB);
ASSERT_EQ(4096_B, 4_KB);
ASSERT_EQ(1048576_B, 1_MB);
ASSERT_EQ(1024_KB, 1_MB);
ASSERT_EQ(1024_MB, 1_GB);
ASSERT_EQ(1024_GB, 1_TB);
ASSERT_EQ(1000_B, 1_K);
ASSERT_EQ(1000_K, 1_M);
ASSERT_EQ(1000_M, 1_G);
ASSERT_EQ(1000_G, 1_T);
ASSERT_EQ(Size::toString(0), "0");
ASSERT_EQ(Size::toString(511), "511");
ASSERT_EQ(Size::toString(512), "512");
ASSERT_EQ(Size::toString(1013), "1013");
ASSERT_EQ(Size::toString(1025), "1025");
ASSERT_EQ(Size::toString(1034), "1034");
ASSERT_EQ(Size::toString(1048576), "1MB");
ASSERT_EQ(Size::toString(1000), "1K");
ASSERT_EQ(Size::toString(1024), "1KB");
ASSERT_EQ(Size::toString(1000_K), "1M");
ASSERT_EQ(Size::toString(1024_K), "1024K");
ASSERT_EQ(Size::toString(1000_G), "1T");
ASSERT_EQ(Size::toString(1024_G), "1024G");
ASSERT_EQ(Size::from("1K").value(), 1_K);
ASSERT_EQ(Size::from("2KB").value(), 2_KB);
ASSERT_EQ(Size::from("3M").value(), 3_M);
ASSERT_EQ(Size::from("4MB").value(), 4_MB);
ASSERT_EQ(Size::from("5G").value(), 5_G);
ASSERT_EQ(Size::from("6GB").value(), 6_GB);
ASSERT_EQ(Size::from("7T").value(), 7_T);
ASSERT_EQ(Size::from("8TB").value(), 8_TB);
ASSERT_EQ(Size::around(0), "0");
ASSERT_EQ(Size::around(511), "511");
ASSERT_EQ(Size::around(512), "0.50KB");
ASSERT_EQ(Size::around(1013), "0.99KB");
ASSERT_EQ(Size::around(1025), "1.00KB");
ASSERT_EQ(Size::around(1034), "1.01KB");
ASSERT_EQ(Size::around(1048576), "1.00MB");
}
TEST(TestSize, Parse) {
Size c0 = Size::from("100").value();
ASSERT_EQ(c0.toString(), "100");
Size c1 = Size::from("2KB").value();
ASSERT_EQ(c1.toString(), "2KB");
Size c2 = Size::from("1001MB").value();
ASSERT_EQ(c2.toString(), "1001MB");
Size c3 = Size::from("45").value();
ASSERT_EQ(c3.toString(), "45");
Size c5 = Size::from("08GB").value();
ASSERT_EQ(c5.toString(), "8GB");
Size c6 = Size::from("1TB").value();
ASSERT_EQ(c6.toString(), "1TB");
ASSERT_FALSE(Size::from("10kb"));
ASSERT_FALSE(Size::from(""));
ASSERT_FALSE(Size::from("GB"));
ASSERT_FALSE(Size::from("1.1MB"));
ASSERT_EQ(fmt::format("{}", 100_KB), "100KB");
}
TEST(TestSize, testConfig) {
Config cfg;
ASSERT_EQ(cfg.size(), 128_KB);
cfg.set_size(512_KB);
ASSERT_EQ(cfg.size(), 512_KB);
toml::table result = toml::parse(R"(
size = "5KB"
)");
ASSERT_TRUE(cfg.update(result));
ASSERT_EQ(cfg.size(), 5_KB);
XLOGF(INFO, "toString: {}", cfg.toString());
result = toml::parse(cfg.toString());
Config other;
ASSERT_TRUE(other.update(result));
ASSERT_EQ(cfg.size(), other.size());
ASSERT_TRUE(cfg.update(toml::parse(R"(
size = 4096
)")));
ASSERT_EQ(cfg.size(), 4_KB);
}
TEST(TestSize, Infinity) {
constexpr auto inf = Size::infinity();
ASSERT_EQ(inf, std::numeric_limits<uint64_t>::max());
ASSERT_EQ(inf.toString(), "infinity");
ASSERT_OK(Size::from("infinity"));
ASSERT_EQ(*Size::from("infinity"), Size::infinity());
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,110 @@
#include <gtest/gtest.h>
#include <string>
#include "common/utils/Status.h"
namespace hf3fs::tests {
namespace {
TEST(Status, testCtor) {
Status s1(1);
ASSERT_EQ(s1.code(), 1);
ASSERT_TRUE(s1.message().empty());
ASSERT_TRUE(!s1.hasPayload());
Status s2(1, "test");
ASSERT_EQ(s2.code(), 1);
ASSERT_EQ(s2.message(), "test");
ASSERT_TRUE(!s2.hasPayload());
ASSERT_EQ(fmt::format("{}", s2), "NotImplemented(1) test");
}
TEST(Status, testPayload) {
Status s0(1);
s0.setPayload<int>(5);
ASSERT_TRUE(s0.hasPayload());
ASSERT_EQ(*s0.payload<int>(), 5);
s0.setPayload<std::string>("abc");
ASSERT_TRUE(s0.hasPayload());
ASSERT_EQ(*s0.payload<std::string>(), "abc");
s0.resetPayload();
ASSERT_TRUE(!s0.hasPayload());
s0.emplacePayload<std::string>(3, 'a');
ASSERT_EQ(*s0.payload<std::string>(), "aaa");
}
TEST(Status, testCopy) {
Status s2(1, "test");
s2.setPayload<std::string>("abc");
ASSERT_EQ(s2.code(), 1);
ASSERT_EQ(s2.message(), "test");
ASSERT_TRUE(s2.hasPayload());
ASSERT_EQ(*s2.payload<std::string>(), "abc");
Status s3 = s2;
ASSERT_EQ(s2.code(), 1);
ASSERT_EQ(s2.message(), "test");
ASSERT_TRUE(s2.hasPayload());
ASSERT_EQ(*s2.payload<std::string>(), "abc");
ASSERT_EQ(s3.code(), 1);
ASSERT_EQ(s3.message(), "test");
ASSERT_TRUE(s3.hasPayload());
ASSERT_EQ(*s3.payload<std::string>(), "abc");
auto *ps3 = &s3;
s3 = *ps3;
ASSERT_EQ(s3.code(), 1);
ASSERT_EQ(s3.message(), "test");
ASSERT_TRUE(s3.hasPayload());
ASSERT_EQ(*s3.payload<std::string>(), "abc");
Status s4(0);
s4 = s3;
ASSERT_EQ(s3.code(), 1);
ASSERT_EQ(s3.message(), "test");
ASSERT_TRUE(s3.hasPayload());
ASSERT_EQ(*s3.payload<std::string>(), "abc");
ASSERT_EQ(s4.code(), 1);
ASSERT_EQ(s4.message(), "test");
ASSERT_TRUE(s4.hasPayload());
ASSERT_EQ(*s4.payload<std::string>(), "abc");
}
TEST(Status, testMove) {
Status s2(1, "test");
s2.setPayload<std::string>("abc");
ASSERT_EQ(s2.code(), 1);
ASSERT_EQ(s2.message(), "test");
ASSERT_TRUE(s2.hasPayload());
ASSERT_EQ(*s2.payload<std::string>(), "abc");
Status s3 = std::move(s2);
ASSERT_EQ(s2.code(), StatusCode::kOK);
ASSERT_TRUE(s2.message().empty());
ASSERT_TRUE(!s2.hasPayload());
ASSERT_EQ(s3.code(), 1);
ASSERT_EQ(s3.message(), "test");
ASSERT_TRUE(s3.hasPayload());
ASSERT_EQ(*s3.payload<std::string>(), "abc");
auto *ps3 = &s3;
s3 = std::move(*ps3);
ASSERT_EQ(s3.code(), 1);
ASSERT_EQ(s3.message(), "test");
ASSERT_TRUE(s3.hasPayload());
ASSERT_EQ(*s3.payload<std::string>(), "abc");
Status s4(0);
s4 = std::move(s3);
ASSERT_EQ(s3.code(), StatusCode::kOK);
ASSERT_TRUE(s3.message().empty());
ASSERT_TRUE(!s3.hasPayload());
ASSERT_EQ(s4.code(), 1);
ASSERT_EQ(s4.message(), "test");
ASSERT_TRUE(s4.hasPayload());
ASSERT_EQ(*s4.payload<std::string>(), "abc");
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,14 @@
#include "common/utils/StatusCode.h"
#include "gtest/gtest.h"
namespace hf3fs {
namespace {
TEST(StatusCode, toString) {
ASSERT_EQ(StatusCode::toString(StatusCode::kOK), "OK");
ASSERT_EQ(StatusCode::toString(TransactionCode::kConflict), "Transaction::Conflict");
ASSERT_EQ(StatusCode::toString(-1), "UnknownStatusCode");
}
} // namespace
} // namespace hf3fs

View File

@@ -0,0 +1,28 @@
#include <string>
#include "common/utils/StrongType.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::tests {
namespace {
STRONG_TYPEDEF(int, TypedInt);
STRONG_TYPEDEF(size_t, TypedUint);
STRONG_TYPEDEF(std::string, TypedString);
TEST(StrongTypedefTest, testComparison) {
TypedInt ti0;
ASSERT_TRUE(ti0 < 1);
ASSERT_TRUE(ti0 > -1);
ASSERT_EQ(ti0, 0);
TypedUint tu0;
ASSERT_TRUE(tu0.toUnderType() < 1);
ASSERT_TRUE(tu0.toUnderType() >= 0);
ASSERT_EQ(tu0, 0);
TypedString ts0, ts1("aa");
ASSERT_TRUE(ts0 < ts1);
ASSERT_TRUE(ts0 < "a");
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,57 @@
#include <cstdlib>
#include <folly/logging/xlog.h>
#include <gtest/gtest.h>
#include "common/serde/Serde.h"
#include "common/utils/SysResource.h"
namespace hf3fs {
TEST(TestSysResource, GetHostname) {
auto result = SysResource::hostname();
ASSERT_TRUE(result);
XLOGF(INFO, "hostname is {}", result.value());
}
TEST(TestSysResource, FDLimit) {
auto result = SysResource::increaseProcessFDLimit(8192);
ASSERT_TRUE(result);
}
// todo: fix fs UUID in docker
TEST(TestSysResource, DISABLED_FileSystemUUID) {
auto result = SysResource::fileSystemUUID();
ASSERT_TRUE(result);
auto &map = *result;
ASSERT_TRUE(!map.empty());
for (auto [deviceId, uuid] : map) {
XLOGF(INFO, "device: {}, uuid: {}", deviceId, uuid);
}
}
TEST(TestSysResource, ScanDiskInfo) {
auto result = SysResource::scanDiskInfo();
ASSERT_TRUE(result);
XLOGF(INFO, "info is {}", serde::toJsonString(*result));
}
TEST(TestSysResource, getAllEnvs) {
::setenv("HF3FS_TEST_ONE", "ONE", 1);
::setenv("HF3FS_TEST_TWO", "TWO", 1);
::setenv("TEST_THREE", "THREE", 1);
auto m = SysResource::getAllEnvs("HF3FS_");
ASSERT_TRUE(!m.empty());
for (const auto &[k, v] : m) XLOGF(INFO, "env {} = {}", k, v);
ASSERT_TRUE(m.contains("HF3FS_TEST_ONE"));
ASSERT_EQ(m["HF3FS_TEST_ONE"], "ONE");
ASSERT_TRUE(m.contains("HF3FS_TEST_TWO"));
ASSERT_EQ(m["HF3FS_TEST_TWO"], "TWO");
ASSERT_TRUE(!m.contains("TEST_THREE"));
}
} // namespace hf3fs

View File

@@ -0,0 +1,32 @@
#include <type_traits>
#include "common/utils/Thief.h"
#include "common/utils/TypeTraits.h"
#include "tests/GtestHelpers.h"
namespace hf3fs::test {
namespace {
class A {
public:
auto &secret() { return secret_; }
private:
int secret_ = 0;
};
template <typename Tag, auto Value>
struct StoreValue : public std::type_identity<thief::steal<Tag, value_identity<Value>>> {};
struct Tag;
template struct StoreValue<Tag, &A::secret_>;
TEST(TestThief, AccessPrivateMember) {
A a;
a.secret() = 20;
ASSERT_EQ(a.*thief::retrieve<Tag>::value, 20);
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,25 @@
#include <gtest/gtest.h>
#include "common/utils/Toml.hpp"
TEST(TestToml, Normal) {
toml::parse_result config = toml::parse(R"(
name = "foo"
[test]
foo = "bar"
val = 123
)");
ASSERT_TRUE(config["name"].is_string());
ASSERT_EQ(config["name"].value_or(std::string_view{}), std::string_view("foo"));
ASSERT_TRUE(config["test"].is_table());
ASSERT_EQ(config["test"]["foo"].value<std::string_view>(), "bar");
ASSERT_EQ(config["test"]["val"].value<int>(), 123);
ASSERT_FALSE(config.contains("not-exists"));
ASSERT_FALSE(config["not-exists"].is_table());
ASSERT_FALSE(config["not-exists"]["not-exists"].is_value());
ASSERT_EQ(config["not-exists"]["not-exists"].value_or(123), 123);
}

View File

@@ -0,0 +1,14 @@
#include "tests/GtestHelpers.h"
#include "toml.hpp"
namespace hf3fs::test {
namespace {
TEST(TestToml11, Normal) {
toml::value v; // not initialized as a table.
v["foo"] = 42; // OK. `v` will be a table.
ASSERT_TRUE(v.is_table());
}
} // namespace
} // namespace hf3fs::test

View File

@@ -0,0 +1,130 @@
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <gtest/gtest.h>
#include "common/utils/Coroutine.h"
#include "common/utils/Tracing.h"
#include "common/utils/TracingEvent.h"
namespace hf3fs::tests {
namespace {
TEST(Tracing, testEventToString) {
ASSERT_EQ(tracing::toString(tracing::kFdbBeginNewTransaction), "Fdb::BeginNewTransaction");
ASSERT_EQ(tracing::toString(tracing::kFdbEndNewTransaction), "Fdb::EndNewTransaction");
ASSERT_EQ(tracing::toString(tracing::kFdbBeginCommitTransaction), "Fdb::BeginCommitTransaction");
ASSERT_EQ(tracing::toString(tracing::kFdbEndCommitTransaction), "Fdb::EndCommitTransaction");
ASSERT_EQ(tracing::toString(tracing::kFdbBeginCancelTransaction), "Fdb::BeginCancelTransaction");
ASSERT_EQ(tracing::toString(tracing::kFdbEndCancelTransaction), "Fdb::EndCancelTransaction");
ASSERT_EQ(tracing::toString(tracing::kMetaLoadInode), "Meta::LoadInode");
ASSERT_EQ(tracing::toString(tracing::kMetaBeginLoadInode), "Meta::BeginLoadInode");
ASSERT_EQ(tracing::toString(tracing::kMetaEndLoadInode), "Meta::EndLoadInode");
ASSERT_EQ(tracing::toString(tracing::kMetaSnapshotLoadInode), "Meta::SnapshotLoadInode");
ASSERT_EQ(tracing::toString(tracing::kMetaBeginSnapshotLoadInode), "Meta::BeginSnapshotLoadInode");
ASSERT_EQ(tracing::toString(tracing::kMetaEndSnapshotLoadInode), "Meta::EndSnapshotLoadInode");
ASSERT_EQ(tracing::toString(tracing::kMetaLoadDirEntry), "Meta::LoadDirEntry");
ASSERT_EQ(tracing::toString(tracing::kMetaBeginLoadDirEntry), "Meta::BeginLoadDirEntry");
ASSERT_EQ(tracing::toString(tracing::kMetaEndLoadDirEntry), "Meta::EndLoadDirEntry");
ASSERT_EQ(tracing::toString(tracing::kMetaSnapshotLoadDirEntry), "Meta::SnapshotLoadDirEntry");
ASSERT_EQ(tracing::toString(tracing::kMetaBeginSnapshotLoadDirEntry), "Meta::BeginSnapshotLoadDirEntry");
ASSERT_EQ(tracing::toString(tracing::kMetaEndSnapshotLoadDirEntry), "Meta::EndSnapshotLoadDirEntry");
}
TEST(Tracing, testScopeGuard) {
tracing::Points points1, points2;
{
SCOPE_SET_TRACING_POINTS(points1);
TRACING_ADD_SCOPE_EVENT(tracing::kFdbNewTransaction);
{
SCOPE_SET_TRACING_POINTS(points2);
TRACING_ADD_SCOPE_EVENT(tracing::kFdbCommitTransaction);
}
TRACING_ADD_EVENT(tracing::kFdbBeginCancelTransaction);
}
TRACING_ADD_EVENT(tracing::kFdbEndCancelTransaction);
auto v1 = points1.extractAll();
ASSERT_EQ(v1.size(), 3);
ASSERT_EQ(v1[0].event(), tracing::kFdbBeginNewTransaction);
ASSERT_EQ(v1[1].event(), tracing::kFdbBeginCancelTransaction);
ASSERT_EQ(v1[2].event(), tracing::kFdbEndNewTransaction);
auto v2 = points2.extractAll();
ASSERT_EQ(v2.size(), 2);
ASSERT_EQ(v2[0].event(), tracing::kFdbBeginCommitTransaction);
ASSERT_EQ(v2[1].event(), tracing::kFdbEndCommitTransaction);
}
TEST(Tracing, testPassThroughCoroutine) {
tracing::Points points;
folly::CPUThreadPoolExecutor executor(1);
{
SCOPE_SET_TRACING_POINTS(points);
folly::coro::blockingWait([&]() -> CoTask<void> {
TRACING_ADD_SCOPE_EVENT(tracing::kFdbNewTransaction);
auto f = []() -> CoTask<void> {
TRACING_ADD_SCOPE_EVENT(tracing::kFdbCommitTransaction);
co_return;
};
co_await f().scheduleOn(folly::getKeepAliveToken(executor));
}());
}
auto v = points.extractAll();
ASSERT_EQ(v.size(), 4);
ASSERT_EQ(v[0].event(), tracing::kFdbBeginNewTransaction);
ASSERT_EQ(v[1].event(), tracing::kFdbBeginCommitTransaction);
ASSERT_EQ(v[2].event(), tracing::kFdbEndCommitTransaction);
ASSERT_EQ(v[3].event(), tracing::kFdbEndNewTransaction);
}
TEST(Tracing, testPointMessage) {
auto t = UtcClock::now();
tracing::Point p0(t, 0);
ASSERT_EQ(p0.timestamp(), t);
ASSERT_EQ(p0.event(), 0);
ASSERT_EQ(p0.message(), "");
std::string_view msg0 = "A very long message which contains more than 16 chars";
p0.setMessage(msg0);
ASSERT_EQ(p0.message(), msg0);
ASSERT_NE(p0.message().data(), msg0.data());
std::string_view msg1 = "A short message";
p0.setMessage(msg1);
ASSERT_EQ(p0.message(), msg1);
}
TEST(Tracing, testPointMove) {
auto t0 = UtcClock::now();
std::string_view msg0 = "A very long message which contains more than 16 chars";
tracing::Point p0(t0, 0, msg0);
ASSERT_EQ(p0.timestamp(), t0);
ASSERT_EQ(p0.event(), 0);
ASSERT_EQ(p0.message(), msg0);
auto t1 = t0 + std::chrono::microseconds(1);
std::string_view msg1 = "Yet another very long message but shorter than msg0";
{
tracing::Point p1(t1, 1, msg1);
tracing::Point p2 = std::move(p1);
ASSERT_EQ(p1.message(), "");
ASSERT_EQ(p2.timestamp(), t1);
ASSERT_EQ(p2.event(), 1);
ASSERT_EQ(p2.message(), msg1);
p0 = std::move(p2);
ASSERT_EQ(p2.message(), "");
}
ASSERT_EQ(p0.timestamp(), t1);
ASSERT_EQ(p0.event(), 1);
ASSERT_EQ(p0.message(), msg1);
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,34 @@
#include <vector>
#include "common/utils/RobinHood.h"
#include "common/utils/TypeTraits.h"
namespace hf3fs {
namespace {
static_assert(is_vector_v<std::vector<int>>, "is vector");
static_assert(!is_vector_v<std::string>, "not vector");
static_assert(is_set_v<std::set<int>>, "is set");
static_assert(!is_set_v<std::string>, "not set");
static_assert(is_map_v<std::map<int, int>>, "is map");
static_assert(!is_map_v<std::vector<int>>, "not map");
static_assert(GenericPair<std::pair<int, int>>, "is generic pair");
static_assert(!GenericPair<std::map<int, int>>, "not generic pair");
static_assert(GenericPair<robin_hood::pair<int, int>>, "is generic pair");
static_assert(Container<robin_hood::unordered_map<int, int>>, "ok");
struct Base {
int x;
};
struct Inherit : Base {
int y;
};
static_assert(std::is_same_v<member_pointer_to_class_t<&Inherit::x>, Base>);
static_assert(std::is_same_v<member_pointer_to_class_t<&Inherit::y>, Inherit>);
} // namespace
} // namespace hf3fs

View File

@@ -0,0 +1,29 @@
#include <stdexcept>
#include <unordered_map>
#include "common/utils/RobinHood.h"
#include "common/utils/UnorderedDense.h"
#include "tests/GtestHelpers.h"
namespace {
template <class Map>
void insertWhileHashIsZero() {
constexpr auto N = 1000;
Map map;
for (auto i = 0; i < N; ++i) {
map[i] = i;
}
}
TEST(TestUnorderedDense, Normal) {
struct Hash {
size_t operator()(int) const { return 0; }
};
insertWhileHashIsZero<ankerl::unordered_dense::map<int, int, Hash>>();
insertWhileHashIsZero<std::unordered_map<int, int, Hash>>();
ASSERT_THROW((insertWhileHashIsZero<robin_hood::unordered_map<int, int, Hash>>()), std::overflow_error);
}
} // namespace

View File

@@ -0,0 +1,17 @@
#include <gtest/gtest.h>
#include "common/utils/UtcTime.h"
namespace hf3fs::tests {
namespace {
TEST(UtcTime, testConvert) {
UtcTime now = UtcClock::now();
int64_t us = now.toMicroseconds();
UtcTime utc = UtcTime::fromMicroseconds(us);
ASSERT_EQ(us, utc.toMicroseconds());
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,124 @@
#include <boost/core/ignore_unused.hpp>
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid.hpp>
#include <cstdlib>
#include <double-conversion/utils.h>
#include <fmt/core.h>
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <sys/wait.h>
#include <unistd.h>
#include <utility>
#include "common/utils/SerDeser.h"
#include "common/utils/Uuid.h"
namespace hf3fs::tests {
namespace {
std::pair<boost::uuids::uuid, boost::uuids::uuid> gen(auto &generator) {
boost::ignore_unused(generator());
int pipefd[2];
EXPECT_NE(pipe(pipefd), -1);
auto pid = fork();
EXPECT_NE(pid, -1);
auto uuid = generator();
fmt::print("{} {}\n", pid == 0 ? "child " : "father", boost::lexical_cast<std::string>(uuid));
if (pid == 0) {
close(pipefd[0]);
write(pipefd[1], uuid.data, sizeof(uuid));
exit(0);
}
close(pipefd[1]);
boost::uuids::uuid another;
read(pipefd[0], another.data, sizeof(another));
int status;
EXPECT_EQ(wait(&status), pid);
return std::make_pair(uuid, another);
}
TEST(Uuid, DISABLED_fork) {
// disable this test by default because it hangs in gitlab CI.
static thread_local boost::uuids::random_generator_mt19937 mt19937;
static thread_local boost::uuids::random_generator random;
fmt::print("random_generator_mt19937\n");
for (size_t i = 0; i < 10; i++) {
auto a = gen(mt19937);
EXPECT_EQ(a.first, a.second); // shouldn't use mt19937
}
fmt::print("random_generator\n");
for (size_t i = 0; i < 10; i++) {
auto b = gen(random);
EXPECT_NE(b.first, b.second);
}
fmt::print("Uuid\n");
for (size_t i = 0; i < 10; i++) {
auto c = gen(Uuid::random);
EXPECT_NE(c.first, c.second);
}
}
TEST(Uuid, testZero) {
uint8_t buffer[Uuid::static_size()] = {0};
Uuid zero = Uuid::zero();
Uuid def = Uuid{};
ASSERT_EQ(memcmp(buffer, zero.data, Uuid::static_size()), 0);
ASSERT_EQ(zero, def);
Uuid zero1 = Uuid::zero();
ASSERT_EQ(zero, zero1);
}
TEST(Uuid, testRandom) {
Uuid a = Uuid::random();
Uuid b = Uuid::random();
ASSERT_NE(a, b);
}
TEST(Uuid, testToString) {
Uuid a = Uuid::random();
std::cout << a.toHexString() << std::endl;
Uuid zero = Uuid::zero();
std::cout << zero.toHexString() << std::endl;
ASSERT_EQ(zero.toHexString(), "00000000-0000-0000-0000-000000000000");
}
TEST(Uuid, Compare) {
for (size_t i = 0; i < 100; i++) {
Uuid a = Uuid::random();
Uuid b = Uuid::random();
ASSERT_EQ((a < b), (b > a));
ASSERT_NE((a < b), (b < a));
ASSERT_TRUE(a > Uuid::zero());
ASSERT_TRUE(b > Uuid::zero());
ASSERT_TRUE(a < Uuid::max());
ASSERT_TRUE(b < Uuid::max());
auto aser = Serializer::serRawArgs(a);
auto bser = Serializer::serRawArgs(b);
ASSERT_EQ((a < b), (aser < bser));
ASSERT_EQ((a > b), (aser > bser));
ASSERT_TRUE(aser > Serializer::serRawArgs(Uuid::zero()));
ASSERT_TRUE(bser > Serializer::serRawArgs(Uuid::zero()));
ASSERT_TRUE(aser < Serializer::serRawArgs(Uuid::max()));
ASSERT_TRUE(bser < Serializer::serRawArgs(Uuid::max()));
}
}
TEST(Uuid, Gen) {
for (size_t i = 0; i < 100; i++) {
fmt::println("{}", Uuid::random());
}
}
} // namespace
} // namespace hf3fs::tests

View File

@@ -0,0 +1,38 @@
#include <gtest/gtest.h>
#include <limits>
#include "common/serde/Serde.h"
#include "common/utils/Varint32.h"
namespace hf3fs::test {
namespace {
TEST(TestVarint32, Normal) {
Varint32 value = 20;
value = 30;
ASSERT_EQ(value, 30);
auto out = serde::serialize(value);
Varint32 de1{};
ASSERT_TRUE(serde::deserialize(de1, out).hasValue());
ASSERT_EQ(de1, value);
Varint64 de2{};
ASSERT_TRUE(serde::deserialize(de2, out).hasValue());
ASSERT_EQ(de2, value);
}
TEST(TestVarint64, Normal) {
for (auto value : {0ul, 1ul << 32, std::numeric_limits<uint64_t>::max()}) {
Varint64 ser = value;
auto out = serde::serialize(ser);
Varint64 der{};
ASSERT_TRUE(serde::deserialize(der, out).hasValue());
ASSERT_EQ(ser, der);
}
}
} // namespace
} // namespace hf3fs::test

Some files were not shown because too many files have changed in this diff Show More