aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/zenserver-test/buildstore-tests.cpp499
-rw-r--r--src/zenserver-test/cache-tests.cpp2365
-rw-r--r--src/zenserver-test/projectstore-tests.cpp1055
-rw-r--r--src/zenserver-test/workspace-tests.cpp541
-rw-r--r--src/zenserver-test/zenserver-test.cpp4636
-rw-r--r--src/zenserver-test/zenserver-test.h207
6 files changed, 4701 insertions, 4602 deletions
diff --git a/src/zenserver-test/buildstore-tests.cpp b/src/zenserver-test/buildstore-tests.cpp
new file mode 100644
index 000000000..86255b15c
--- /dev/null
+++ b/src/zenserver-test/buildstore-tests.cpp
@@ -0,0 +1,499 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#if ZEN_WITH_TESTS
+# include "zenserver-test.h"
+# include <zencore/testing.h>
+# include <zencore/testutils.h>
+# include <zencore/workthreadpool.h>
+# include <zencore/compactbinarybuilder.h>
+# include <zencore/compactbinarypackage.h>
+# include <zencore/compress.h>
+# include <zencore/filesystem.h>
+# include <zencore/stream.h>
+# include <zencore/string.h>
+# include <tsl/robin_set.h>
+# include <zencore/fmtutils.h>
+# include <zencore/scopeguard.h>
+# include <zenhttp/packageformat.h>
+# include <zenutil/buildstoragecache.h>
+# include <zenutil/zenserverprocess.h>
+# include <zenhttp/httpclient.h>
+
+namespace zen::tests {
+
+using namespace std::literals;
+
+TEST_CASE("buildstore.blobs")
+{
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+ auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); });
+
+ std::string_view Namespace = "ns"sv;
+ std::string_view Bucket = "bkt"sv;
+ Oid BuildId = Oid::NewOid();
+
+ std::vector<IoHash> CompressedBlobsHashes;
+ {
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri() + "/builds/");
+
+ for (size_t I = 0; I < 5; I++)
+ {
+ IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7);
+ CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
+ CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash());
+ IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer();
+ Payload.SetContentType(ZenContentType::kCompressedBinary);
+
+ HttpClient::Response Result =
+ Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload);
+ CHECK(Result);
+ }
+
+ for (const IoHash& RawHash : CompressedBlobsHashes)
+ {
+ HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash),
+ HttpClient::Accept(ZenContentType::kCompressedBinary));
+ CHECK(Result);
+ IoBuffer Payload = Result.ResponsePayload;
+ CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary);
+ IoHash VerifyRawHash;
+ uint64_t VerifyRawSize;
+ CompressedBuffer CompressedBlob =
+ CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize);
+ CHECK(CompressedBlob);
+ CHECK(VerifyRawHash == RawHash);
+ IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer();
+ CHECK(IoHash::HashBuffer(Decompressed) == RawHash);
+ }
+ }
+ {
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri() + "/builds/");
+
+ for (const IoHash& RawHash : CompressedBlobsHashes)
+ {
+ HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash),
+ HttpClient::Accept(ZenContentType::kCompressedBinary));
+ CHECK(Result);
+ IoBuffer Payload = Result.ResponsePayload;
+ CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary);
+ IoHash VerifyRawHash;
+ uint64_t VerifyRawSize;
+ CompressedBuffer CompressedBlob =
+ CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize);
+ CHECK(CompressedBlob);
+ CHECK(VerifyRawHash == RawHash);
+ IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer();
+ CHECK(IoHash::HashBuffer(Decompressed) == RawHash);
+ }
+
+ for (size_t I = 0; I < 5; I++)
+ {
+ IoBuffer Blob = CreateSemiRandomBlob(5713 + I * 7);
+ CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
+ CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash());
+ IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer();
+ Payload.SetContentType(ZenContentType::kCompressedBinary);
+
+ HttpClient::Response Result =
+ Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload);
+ CHECK(Result);
+ }
+ }
+ {
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri() + "/builds/");
+
+ for (const IoHash& RawHash : CompressedBlobsHashes)
+ {
+ HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash),
+ HttpClient::Accept(ZenContentType::kCompressedBinary));
+ CHECK(Result);
+ IoBuffer Payload = Result.ResponsePayload;
+ CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary);
+ IoHash VerifyRawHash;
+ uint64_t VerifyRawSize;
+ CompressedBuffer CompressedBlob =
+ CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize);
+ CHECK(CompressedBlob);
+ CHECK(VerifyRawHash == RawHash);
+ IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer();
+ CHECK(IoHash::HashBuffer(Decompressed) == RawHash);
+ }
+ }
+}
+
+namespace {
+ CbObject MakeMetadata(const IoHash& BlobHash, const std::vector<std::pair<std::string, std::string>>& KeyValues)
+ {
+ CbObjectWriter Writer;
+ Writer.AddHash("rawHash"sv, BlobHash);
+ Writer.BeginObject("values");
+ {
+ for (const auto& V : KeyValues)
+ {
+ Writer.AddString(V.first, V.second);
+ }
+ }
+ Writer.EndObject(); // values
+ return Writer.Save();
+ };
+
+} // namespace
+
+TEST_CASE("buildstore.metadata")
+{
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+ auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); });
+
+ std::string_view Namespace = "ns"sv;
+ std::string_view Bucket = "bkt"sv;
+ Oid BuildId = Oid::NewOid();
+
+ std::vector<IoHash> BlobHashes;
+ std::vector<CbObject> Metadatas;
+ std::vector<IoHash> MetadataHashes;
+
+ auto GetMetadatas =
+ [](HttpClient& Client, std::string_view Namespace, std::string_view Bucket, const Oid& BuildId, std::vector<IoHash> BlobHashes) {
+ CbObjectWriter Request;
+
+ Request.BeginArray("blobHashes"sv);
+ for (const IoHash& BlobHash : BlobHashes)
+ {
+ Request.AddHash(BlobHash);
+ }
+ Request.EndArray();
+
+ IoBuffer Payload = Request.Save().GetBuffer().AsIoBuffer();
+ Payload.SetContentType(ZenContentType::kCbObject);
+
+ HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/getBlobMetadata", Namespace, Bucket, BuildId),
+ Payload,
+ HttpClient::Accept(ZenContentType::kCbObject));
+ CHECK(Result);
+
+ std::vector<CbObject> ResultMetadatas;
+
+ CbPackage ResponsePackage = ParsePackageMessage(Result.ResponsePayload);
+ CbObject ResponseObject = ResponsePackage.GetObject();
+
+ CbArrayView BlobHashArray = ResponseObject["blobHashes"sv].AsArrayView();
+ CbArrayView MetadatasArray = ResponseObject["metadatas"sv].AsArrayView();
+ ResultMetadatas.reserve(MetadatasArray.Num());
+ auto BlobHashesIt = BlobHashes.begin();
+ auto BlobHashArrayIt = begin(BlobHashArray);
+ auto MetadataArrayIt = begin(MetadatasArray);
+ while (MetadataArrayIt != end(MetadatasArray))
+ {
+ const IoHash BlobHash = (*BlobHashArrayIt).AsHash();
+ while (BlobHash != *BlobHashesIt)
+ {
+ ZEN_ASSERT(BlobHashesIt != BlobHashes.end());
+ BlobHashesIt++;
+ }
+
+ ZEN_ASSERT(BlobHash == *BlobHashesIt);
+
+ const IoHash MetaHash = (*MetadataArrayIt).AsAttachment();
+ const CbAttachment* MetaAttachment = ResponsePackage.FindAttachment(MetaHash);
+ ZEN_ASSERT(MetaAttachment);
+
+ CbObject Metadata = MetaAttachment->AsObject();
+ ResultMetadatas.emplace_back(std::move(Metadata));
+
+ BlobHashArrayIt++;
+ MetadataArrayIt++;
+ BlobHashesIt++;
+ }
+ return ResultMetadatas;
+ };
+
+ {
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri() + "/builds/");
+
+ const size_t BlobCount = 5;
+
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ BlobHashes.push_back(IoHash::HashBuffer(&I, sizeof(I)));
+ Metadatas.push_back(MakeMetadata(BlobHashes.back(), {{"index", fmt::format("{}", I)}}));
+ MetadataHashes.push_back(IoHash::HashBuffer(Metadatas.back().GetBuffer().AsIoBuffer()));
+ }
+
+ {
+ CbPackage RequestPackage;
+ std::vector<CbAttachment> Attachments;
+ tsl::robin_set<IoHash, IoHash::Hasher> AttachmentHashes;
+ Attachments.reserve(BlobCount);
+ AttachmentHashes.reserve(BlobCount);
+ {
+ CbObjectWriter RequestWriter;
+ RequestWriter.BeginArray("blobHashes");
+ for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++)
+ {
+ RequestWriter.AddHash(BlobHashes[BlockHashIndex]);
+ }
+ RequestWriter.EndArray(); // blobHashes
+
+ RequestWriter.BeginArray("metadatas");
+ for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++)
+ {
+ const IoHash ObjectHash = Metadatas[BlockHashIndex].GetHash();
+ RequestWriter.AddBinaryAttachment(ObjectHash);
+ if (!AttachmentHashes.contains(ObjectHash))
+ {
+ Attachments.push_back(CbAttachment(Metadatas[BlockHashIndex], ObjectHash));
+ AttachmentHashes.insert(ObjectHash);
+ }
+ }
+
+ RequestWriter.EndArray(); // metadatas
+
+ RequestPackage.SetObject(RequestWriter.Save());
+ }
+ RequestPackage.AddAttachments(Attachments);
+
+ CompositeBuffer RpcRequestBuffer = FormatPackageMessageBuffer(RequestPackage);
+
+ HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/putBlobMetadata", Namespace, Bucket, BuildId),
+ RpcRequestBuffer,
+ ZenContentType::kCbPackage);
+ CHECK(Result);
+ }
+
+ {
+ std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes);
+
+ for (size_t Index = 0; Index < MetadataHashes.size(); Index++)
+ {
+ const IoHash& ExpectedHash = MetadataHashes[Index];
+ IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer());
+ CHECK_EQ(ExpectedHash, Hash);
+ }
+ }
+ }
+ {
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri() + "/builds/");
+
+ std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes);
+
+ for (size_t Index = 0; Index < MetadataHashes.size(); Index++)
+ {
+ const IoHash& ExpectedHash = MetadataHashes[Index];
+ IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer());
+ CHECK_EQ(ExpectedHash, Hash);
+ }
+ }
+}
+
+TEST_CASE("buildstore.cache")
+{
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+ std::filesystem::path TempDir = TestEnv.CreateNewTestDir();
+ auto _ = MakeGuard([&SystemRootPath, &TempDir]() {
+ DeleteDirectories(SystemRootPath);
+ DeleteDirectories(TempDir);
+ });
+
+ std::string_view Namespace = "ns"sv;
+ std::string_view Bucket = "bkt"sv;
+ Oid BuildId = Oid::NewOid();
+
+ std::vector<IoHash> BlobHashes;
+ std::vector<CbObject> Metadatas;
+ std::vector<IoHash> MetadataHashes;
+
+ const size_t BlobCount = 5;
+ {
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+
+ BuildStorageCache::Statistics Stats;
+ std::unique_ptr<BuildStorageCache> Cache(CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false));
+
+ {
+ IoHash NoneBlob = IoHash::HashBuffer("data", 4);
+ std::vector<BuildStorageCache::BlobExistsResult> NoneExists = Cache->BlobsExists(BuildId, std::vector<IoHash>{NoneBlob});
+ CHECK(NoneExists.size() == 1);
+ CHECK(!NoneExists[0].HasBody);
+ CHECK(!NoneExists[0].HasMetadata);
+ }
+
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7);
+ CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
+ BlobHashes.push_back(CompressedBlob.DecodeRawHash());
+ Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed());
+ }
+
+ Cache->Flush(500);
+ Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false);
+
+ {
+ std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
+ CHECK(Exists.size() == BlobHashes.size());
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ CHECK(Exists[I].HasBody);
+ CHECK(!Exists[I].HasMetadata);
+ }
+
+ std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
+ CHECK_EQ(0, FetchedMetadatas.size());
+ }
+
+ {
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]);
+ CHECK(BuildBlob);
+ CHECK_EQ(BlobHashes[I],
+ IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer()));
+ }
+ }
+
+ {
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ CbObject Metadata = MakeMetadata(BlobHashes[I],
+ {{"key", fmt::format("{}", I)},
+ {"key_plus_one", fmt::format("{}", I + 1)},
+ {"block_hash", fmt::format("{}", BlobHashes[I])}});
+ Metadatas.push_back(Metadata);
+ MetadataHashes.push_back(IoHash::HashBuffer(Metadata.GetBuffer().AsIoBuffer()));
+ }
+ Cache->PutBlobMetadatas(BuildId, BlobHashes, Metadatas);
+ }
+
+ Cache->Flush(500);
+ Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false);
+
+ {
+ std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
+ CHECK(Exists.size() == BlobHashes.size());
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ CHECK(Exists[I].HasBody);
+ CHECK(Exists[I].HasMetadata);
+ }
+
+ std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
+ CHECK_EQ(BlobCount, FetchedMetadatas.size());
+
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer()));
+ }
+ }
+
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7);
+ CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
+ BlobHashes.push_back(CompressedBlob.DecodeRawHash());
+ Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed());
+ }
+
+ Cache->Flush(500);
+ Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false);
+
+ {
+ std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
+ CHECK(Exists.size() == BlobHashes.size());
+ for (size_t I = 0; I < BlobCount * 2; I++)
+ {
+ CHECK(Exists[I].HasBody);
+ CHECK_EQ(I < BlobCount, Exists[I].HasMetadata);
+ }
+
+ std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
+ CHECK_EQ(BlobCount, MetaDatas.size());
+
+ std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
+ CHECK_EQ(BlobCount, FetchedMetadatas.size());
+
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer()));
+ }
+ }
+ }
+
+ {
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+
+ BuildStorageCache::Statistics Stats;
+ std::unique_ptr<BuildStorageCache> Cache(CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false));
+
+ std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
+ CHECK(Exists.size() == BlobHashes.size());
+ for (size_t I = 0; I < BlobCount * 2; I++)
+ {
+ CHECK(Exists[I].HasBody);
+ CHECK_EQ(I < BlobCount, Exists[I].HasMetadata);
+ }
+
+ for (size_t I = 0; I < BlobCount * 2; I++)
+ {
+ IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]);
+ CHECK(BuildBlob);
+ CHECK_EQ(BlobHashes[I],
+ IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer()));
+ }
+
+ std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
+ CHECK_EQ(BlobCount, MetaDatas.size());
+
+ std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
+ CHECK_EQ(BlobCount, FetchedMetadatas.size());
+
+ for (size_t I = 0; I < BlobCount; I++)
+ {
+ CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer()));
+ }
+ }
+}
+
+} // namespace zen::tests
+#endif
diff --git a/src/zenserver-test/cache-tests.cpp b/src/zenserver-test/cache-tests.cpp
new file mode 100644
index 000000000..da0ef6b1d
--- /dev/null
+++ b/src/zenserver-test/cache-tests.cpp
@@ -0,0 +1,2365 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#if ZEN_WITH_TESTS
+# include "zenserver-test.h"
+# include <zencore/testing.h>
+# include <zencore/testutils.h>
+# include <zencore/workthreadpool.h>
+# include <zencore/compactbinarybuilder.h>
+# include <zencore/compactbinarypackage.h>
+# include <zencore/compress.h>
+# include <zencore/fmtutils.h>
+# include <zenhttp/packageformat.h>
+# include <zenutil/cache/cachepolicy.h>
+# include <zenutil/cache/cacherequests.h>
+# include <zencore/filesystem.h>
+# include <zencore/stream.h>
+# include <zencore/string.h>
+# include <zenutil/zenserverprocess.h>
+# include <zenhttp/httpclient.h>
+
+# include <random>
+
+namespace zen::tests {
+
+TEST_CASE("zcache.basic")
+{
+ using namespace std::literals;
+
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+
+ const int kIterationCount = 100;
+
+ auto HashKey = [](int i) -> zen::IoHash { return zen::IoHash::HashBuffer(&i, sizeof i); };
+
+ {
+ ZenServerInstance Instance1(TestEnv);
+ Instance1.SetTestDir(TestDir);
+
+ const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
+ const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber);
+
+ // Populate with some simple data
+
+ HttpClient Http{BaseUri};
+
+ for (int i = 0; i < kIterationCount; ++i)
+ {
+ zen::CbObjectWriter Cbo;
+ Cbo << "index" << i;
+
+ IoBuffer Payload = Cbo.Save().GetBuffer().AsIoBuffer();
+ Payload.SetContentType(HttpContentType::kCbObject);
+
+ zen::IoHash Key = HashKey(i);
+
+ HttpClient::Response Result = Http.Put(fmt::format("/test/{}", Key), Payload);
+
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ // Retrieve data
+
+ for (int i = 0; i < kIterationCount; ++i)
+ {
+ zen::IoHash Key = HashKey(i);
+
+ HttpClient::Response Result = Http.Get(fmt::format("/test/{}", Key), {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+
+ // Ensure bad bucket identifiers are rejected
+
+ {
+ zen::CbObjectWriter Cbo;
+ Cbo << "index" << 42;
+
+ IoBuffer Payload = Cbo.Save().GetBuffer().AsIoBuffer();
+ Payload.SetContentType(HttpContentType::kCbObject);
+
+ zen::IoHash Key = HashKey(442);
+
+ HttpClient::Response Result = Http.Put(fmt::format("/te!st/{}", Key), Payload);
+
+ CHECK(Result.StatusCode == HttpResponseCode::BadRequest);
+ }
+ }
+
+ // Verify that the data persists between process runs (the previous server has exited at this point)
+
+ {
+ ZenServerInstance Instance1(TestEnv);
+ Instance1.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
+
+ const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber);
+
+ HttpClient Http{BaseUri};
+
+ // Retrieve data again
+
+ for (int i = 0; i < kIterationCount; ++i)
+ {
+ zen::IoHash Key = HashKey(i);
+
+ HttpClient::Response Result = Http.Get(fmt::format("/{}/{}", "test", Key), {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+ }
+}
+
+TEST_CASE("zcache.cbpackage")
+{
+ using namespace std::literals;
+
+ auto CreateTestPackage = [](zen::IoHash& OutAttachmentKey) -> zen::CbPackage {
+ auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9}));
+ auto CompressedData = zen::CompressedBuffer::Compress(Data);
+
+ OutAttachmentKey = CompressedData.DecodeRawHash();
+
+ zen::CbWriter Obj;
+ Obj.BeginObject("obj"sv);
+ Obj.AddBinaryAttachment("data", OutAttachmentKey);
+ Obj.EndObject();
+
+ zen::CbPackage Package;
+ Package.SetObject(Obj.Save().AsObject());
+ Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey));
+
+ return Package;
+ };
+
+ auto IsEqual = [](zen::CbPackage Lhs, zen::CbPackage Rhs) -> bool {
+ std::span<const zen::CbAttachment> LhsAttachments = Lhs.GetAttachments();
+ std::span<const zen::CbAttachment> RhsAttachments = Rhs.GetAttachments();
+
+ if (LhsAttachments.size() != RhsAttachments.size())
+ {
+ return false;
+ }
+
+ for (const zen::CbAttachment& LhsAttachment : LhsAttachments)
+ {
+ const zen::CbAttachment* RhsAttachment = Rhs.FindAttachment(LhsAttachment.GetHash());
+ CHECK(RhsAttachment);
+
+ zen::SharedBuffer LhsBuffer = LhsAttachment.AsCompressedBinary().Decompress();
+ CHECK(!LhsBuffer.IsNull());
+
+ zen::SharedBuffer RhsBuffer = RhsAttachment->AsCompressedBinary().Decompress();
+ CHECK(!RhsBuffer.IsNull());
+
+ if (!LhsBuffer.GetView().EqualBytes(RhsBuffer.GetView()))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ };
+
+ SUBCASE("PUT/GET returns correct package")
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance Instance1(TestEnv);
+ Instance1.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
+ const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber);
+
+ HttpClient Http{BaseUri};
+
+ const std::string_view Bucket = "mosdef"sv;
+ zen::IoHash Key;
+ zen::CbPackage ExpectedPackage = CreateTestPackage(Key);
+
+ // PUT
+ {
+ zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage);
+ HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), Body);
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ // GET
+ {
+ HttpClient::Response Result = Http.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+
+ zen::CbPackage Package;
+ const bool Ok = Package.TryLoad(Result.ResponsePayload);
+ CHECK(Ok);
+ CHECK(IsEqual(Package, ExpectedPackage));
+ }
+ }
+
+ SUBCASE("PUT propagates upstream")
+ {
+ // Setup local and remote server
+ std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir();
+ std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance RemoteInstance(TestEnv);
+ RemoteInstance.SetTestDir(RemoteDataDir);
+ const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady();
+
+ ZenServerInstance LocalInstance(TestEnv);
+ LocalInstance.SetTestDir(LocalDataDir);
+ LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(),
+ fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber));
+ const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady();
+ CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput());
+
+ const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber);
+ const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber);
+
+ const std::string_view Bucket = "mosdef"sv;
+ zen::IoHash Key;
+ zen::CbPackage ExpectedPackage = CreateTestPackage(Key);
+
+ HttpClient LocalHttp{LocalBaseUri};
+ HttpClient RemoteHttp{RemoteBaseUri};
+
+ // Store the cache record package in the local instance
+ {
+ zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage);
+ HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}", Bucket, Key), Body);
+
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ // The cache record can be retrieved as a package from the local instance
+ {
+ HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+
+ zen::CbPackage Package;
+ const bool Ok = Package.TryLoad(Result.ResponsePayload);
+ CHECK(Ok);
+ CHECK(IsEqual(Package, ExpectedPackage));
+ }
+
+ // The cache record can be retrieved as a package from the remote instance
+ {
+ HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+
+ zen::CbPackage Package;
+ const bool Ok = Package.TryLoad(Result.ResponsePayload);
+ CHECK(Ok);
+ CHECK(IsEqual(Package, ExpectedPackage));
+ }
+ }
+
+ SUBCASE("GET finds upstream when missing in local")
+ {
+ // Setup local and remote server
+ std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir();
+ std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance RemoteInstance(TestEnv);
+ RemoteInstance.SetTestDir(RemoteDataDir);
+ const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady();
+
+ ZenServerInstance LocalInstance(TestEnv);
+ LocalInstance.SetTestDir(LocalDataDir);
+ LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(),
+ fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber));
+ const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady();
+ CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput());
+
+ const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber);
+ const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber);
+
+ HttpClient LocalHttp{LocalBaseUri};
+ HttpClient RemoteHttp{RemoteBaseUri};
+
+ const std::string_view Bucket = "mosdef"sv;
+ zen::IoHash Key;
+ zen::CbPackage ExpectedPackage = CreateTestPackage(Key);
+
+ // Store the cache record package in upstream cache
+ {
+ zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage);
+ HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), Body);
+
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ // The cache record can be retrieved as a package from the local cache
+ {
+ HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+
+ zen::CbPackage Package;
+ const bool Ok = Package.TryLoad(Result.ResponsePayload);
+ CHECK(Ok);
+ CHECK(IsEqual(Package, ExpectedPackage));
+ }
+ }
+}
+
+TEST_CASE("zcache.policy")
+{
+ using namespace std::literals;
+ using namespace utils;
+
+ auto GenerateData = [](uint64_t Size, zen::IoHash& OutHash) -> zen::IoBuffer {
+ auto Buf = zen::UniqueBuffer::Alloc(Size);
+ uint8_t* Data = reinterpret_cast<uint8_t*>(Buf.GetData());
+ for (uint64_t Idx = 0; Idx < Size; Idx++)
+ {
+ Data[Idx] = Idx % 256;
+ }
+ OutHash = zen::IoHash::HashBuffer(Data, Size);
+ return Buf.MoveToShared().AsIoBuffer();
+ };
+
+ auto GeneratePackage = [](zen::IoHash& OutRecordKey, zen::IoHash& OutAttachmentKey) -> zen::CbPackage {
+ auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9}));
+ auto CompressedData = zen::CompressedBuffer::Compress(Data);
+ OutAttachmentKey = CompressedData.DecodeRawHash();
+
+ zen::CbWriter Writer;
+ Writer.BeginObject("obj"sv);
+ Writer.AddBinaryAttachment("data", OutAttachmentKey);
+ Writer.EndObject();
+ CbObject CacheRecord = Writer.Save().AsObject();
+
+ OutRecordKey = IoHash::HashBuffer(CacheRecord.GetBuffer().GetView());
+
+ zen::CbPackage Package;
+ Package.SetObject(CacheRecord);
+ Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey));
+
+ return Package;
+ };
+
+ SUBCASE("query - 'local' does not query upstream (binary)")
+ {
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamInst(TestEnv);
+ UpstreamCfg.Spawn(UpstreamInst);
+ const uint16_t UpstreamPort = UpstreamCfg.Port;
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort);
+ ZenServerInstance LocalInst(TestEnv);
+ LocalCfg.Spawn(LocalInst);
+
+ const std::string_view Bucket = "legacy"sv;
+
+ zen::IoHash Key;
+ IoBuffer BinaryValue = GenerateData(1024, Key);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient RemoteHttp{UpstreamCfg.BaseUri};
+
+ {
+ HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), BinaryValue);
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ HttpClient::Response Result =
+ LocalHttp.Get(fmt::format("/{}/{}?Policy=QueryLocal,Store", Bucket, Key), {{"Accept", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::NotFound);
+ }
+
+ {
+ HttpClient::Response Result =
+ LocalHttp.Get(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), {{"Accept", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+ }
+
+ SUBCASE("store - 'local' does not store upstream (binary)")
+ {
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamInst(TestEnv);
+ UpstreamCfg.Spawn(UpstreamInst);
+ const uint16_t UpstreamPort = UpstreamCfg.Port;
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort);
+ ZenServerInstance LocalInst(TestEnv);
+ LocalCfg.Spawn(LocalInst);
+
+ const auto Bucket = "legacy"sv;
+
+ zen::IoHash Key;
+ IoBuffer BinaryValue = GenerateData(1024, Key);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient RemoteHttp{UpstreamCfg.BaseUri};
+
+ // Store binary cache value locally
+ {
+ HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,StoreLocal", Bucket, Key),
+ BinaryValue,
+ {{"Content-Type", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::NotFound);
+ }
+
+ {
+ HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+ }
+
+ SUBCASE("store - 'local/remote' stores local and upstream (binary)")
+ {
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamInst(TestEnv);
+ UpstreamCfg.Spawn(UpstreamInst);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalInst(TestEnv);
+ LocalCfg.Spawn(LocalInst);
+
+ const auto Bucket = "legacy"sv;
+
+ zen::IoHash Key;
+ IoBuffer BinaryValue = GenerateData(1024, Key);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient RemoteHttp{UpstreamCfg.BaseUri};
+
+ // Store binary cache value locally and upstream
+ {
+ HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key),
+ BinaryValue,
+ {{"Content-Type", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+
+ {
+ HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+ }
+
+ SUBCASE("query - 'local' does not query upstream (cbpackage)")
+ {
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamInst(TestEnv);
+ UpstreamCfg.Spawn(UpstreamInst);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalInst(TestEnv);
+ LocalCfg.Spawn(LocalInst);
+
+ const auto Bucket = "legacy"sv;
+
+ zen::IoHash Key;
+ zen::IoHash PayloadId;
+ zen::CbPackage Package = GeneratePackage(Key, PayloadId);
+ IoBuffer Buf = SerializeToBuffer(Package);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient RemoteHttp{UpstreamCfg.BaseUri};
+
+ // Store package upstream
+ {
+ HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), Buf);
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ HttpClient::Response Result =
+ LocalHttp.Get(fmt::format("/{}/{}?Policy=QueryLocal,Store", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::NotFound);
+ }
+
+ {
+ HttpClient::Response Result =
+ LocalHttp.Get(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+ }
+
+ SUBCASE("store - 'local' does not store upstream (cbpackage)")
+ {
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamInst(TestEnv);
+ UpstreamCfg.Spawn(UpstreamInst);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalInst(TestEnv);
+ LocalCfg.Spawn(LocalInst);
+
+ const auto Bucket = "legacy"sv;
+
+ zen::IoHash Key;
+ zen::IoHash PayloadId;
+ zen::CbPackage Package = GeneratePackage(Key, PayloadId);
+ IoBuffer Buf = SerializeToBuffer(Package);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient RemoteHttp{UpstreamCfg.BaseUri};
+
+ // Store package locally
+ {
+ HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,StoreLocal", Bucket, Key), Buf);
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::NotFound);
+ }
+
+ {
+ HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+ }
+
+ SUBCASE("store - 'local/remote' stores local and upstream (cbpackage)")
+ {
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamInst(TestEnv);
+ UpstreamCfg.Spawn(UpstreamInst);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalInst(TestEnv);
+ LocalCfg.Spawn(LocalInst);
+
+ const auto Bucket = "legacy"sv;
+
+ zen::IoHash Key;
+ zen::IoHash PayloadId;
+ zen::CbPackage Package = GeneratePackage(Key, PayloadId);
+ IoBuffer Buf = SerializeToBuffer(Package);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient RemoteHttp{UpstreamCfg.BaseUri};
+
+ // Store package locally and upstream
+ {
+ HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), Buf);
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+
+ {
+ HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ }
+ }
+
+ SUBCASE("skip - 'data' returns cache record without attachments/empty payload")
+ {
+ ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance Instance(TestEnv);
+ Cfg.Spawn(Instance);
+
+ const auto Bucket = "test"sv;
+
+ zen::IoHash Key;
+ zen::IoHash PayloadId;
+ zen::CbPackage Package = GeneratePackage(Key, PayloadId);
+ IoBuffer Buf = SerializeToBuffer(Package);
+
+ HttpClient Http{Cfg.BaseUri};
+
+ // Store package
+ {
+ HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), Buf);
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ // Get package
+ {
+ HttpClient::Response Result =
+ Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Result);
+ CbPackage ResponsePackage;
+ CHECK(ResponsePackage.TryLoad(Result.ResponsePayload));
+ CHECK(ResponsePackage.GetAttachments().size() == 0);
+ }
+
+ // Get record
+ {
+ HttpClient::Response Result =
+ Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/x-ue-cb"}});
+ CHECK(Result);
+ CbObject ResponseObject = zen::LoadCompactBinaryObject(Result.ResponsePayload);
+ CHECK(ResponseObject);
+ }
+
+ // Get payload
+ {
+ HttpClient::Response Result =
+ Http.Get(fmt::format("/{}/{}/{}?Policy=Default,SkipData", Bucket, Key, PayloadId), {{"Accept", "application/x-ue-comp"}});
+ CHECK(Result);
+ CHECK(Result.ResponsePayload.GetSize() == 0);
+ }
+ }
+
+ SUBCASE("skip - 'data' returns empty binary value")
+ {
+ ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance Instance(TestEnv);
+ Cfg.Spawn(Instance);
+
+ const auto Bucket = "test"sv;
+
+ zen::IoHash Key;
+ IoBuffer BinaryValue = GenerateData(1024, Key);
+
+ HttpClient Http{Cfg.BaseUri};
+
+ // Store binary cache value
+ {
+ HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), BinaryValue);
+ CHECK(Result.StatusCode == HttpResponseCode::Created);
+ }
+
+ // Get package
+ {
+ HttpClient::Response Result =
+ Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/octet-stream"}});
+ CHECK(Result);
+ CHECK(Result.ResponsePayload.GetSize() == 0);
+ }
+ }
+}
+
+TEST_CASE("zcache.rpc")
+{
+ using namespace std::literals;
+
+ auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request,
+ const zen::CacheKey& CacheKey,
+ size_t PayloadSize,
+ CachePolicy RecordPolicy) {
+ std::vector<uint8_t> Data;
+ Data.resize(PayloadSize);
+ uint32_t DataSeed = *reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0]);
+ uint16_t* DataPtr = reinterpret_cast<uint16_t*>(Data.data());
+ for (size_t Idx = 0; Idx < PayloadSize / 2; ++Idx)
+ {
+ DataPtr[Idx] = static_cast<uint16_t>((Idx + DataSeed) % 0xffffu);
+ }
+ if (PayloadSize & 1)
+ {
+ Data[PayloadSize - 1] = static_cast<uint8_t>((PayloadSize - 1) & 0xff);
+ }
+ CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size()));
+ Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy});
+ };
+
+ auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri,
+ std::string_view Namespace,
+ std::string_view Bucket,
+ size_t Num,
+ size_t PayloadSize = 1024,
+ size_t KeyOffset = 1,
+ CachePolicy PutPolicy = CachePolicy::Default,
+ std::vector<CbPackage>* OutPackages = nullptr) -> std::vector<CacheKey> {
+ std::vector<zen::CacheKey> OutKeys;
+
+ HttpClient Http{BaseUri};
+
+ for (uint32_t Key = 1; Key <= Num; ++Key)
+ {
+ zen::IoHash KeyHash;
+ ((uint32_t*)(KeyHash.Hash))[0] = gsl::narrow<uint32_t>(KeyOffset + Key);
+ const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash);
+
+ cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
+ AppendCacheRecord(Request, CacheKey, PayloadSize, PutPolicy);
+ OutKeys.push_back(CacheKey);
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ if (OutPackages)
+ {
+ OutPackages->emplace_back(std::move(Package));
+ }
+ }
+
+ return OutKeys;
+ };
+
+ struct GetCacheRecordResult
+ {
+ zen::CbPackage Response;
+ cacherequests::GetCacheRecordsResult Result;
+ bool Success;
+ };
+
+ auto GetCacheRecords = [](std::string_view BaseUri,
+ std::string_view Namespace,
+ std::span<zen::CacheKey> Keys,
+ zen::CachePolicy Policy,
+ zen::RpcAcceptOptions AcceptOptions = zen::RpcAcceptOptions::kNone,
+ int Pid = 0) -> GetCacheRecordResult {
+ cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
+ .AcceptOptions = static_cast<uint16_t>(AcceptOptions),
+ .ProcessPid = Pid,
+ .DefaultPolicy = Policy,
+ .Namespace = std::string(Namespace)};
+ for (const CacheKey& Key : Keys)
+ {
+ Request.Requests.push_back({.Key = Key});
+ }
+
+ CbObjectWriter RequestWriter;
+ CHECK(Request.Format(RequestWriter));
+
+ IoBuffer Body = RequestWriter.Save().GetBuffer().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbObject);
+
+ HttpClient Http{BaseUri};
+
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ GetCacheRecordResult OutResult;
+
+ if (Result.StatusCode == HttpResponseCode::OK)
+ {
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ CHECK(!Response.IsNull());
+ OutResult.Response = std::move(Response);
+ CHECK(OutResult.Result.Parse(OutResult.Response));
+ OutResult.Success = true;
+ }
+
+ return OutResult;
+ };
+
+ SUBCASE("get cache records")
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance Inst(TestEnv);
+ Inst.SetTestDir(TestDir);
+
+ const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady();
+ const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort);
+
+ CachePolicy Policy = CachePolicy::Default;
+ std::vector<zen::CacheKey> Keys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 128);
+ GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record);
+ CHECK(Record->Key == ExpectedKey);
+ CHECK(Record->Values.size() == 1);
+
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ CHECK(Value.Body);
+ }
+ }
+ }
+
+ SUBCASE("get missing cache records")
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance Inst(TestEnv);
+ Inst.SetTestDir(TestDir);
+ const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady();
+ const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort);
+
+ CachePolicy Policy = CachePolicy::Default;
+ std::vector<zen::CacheKey> ExistingKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 128);
+ std::vector<zen::CacheKey> Keys;
+
+ for (const zen::CacheKey& Key : ExistingKeys)
+ {
+ Keys.push_back(Key);
+ Keys.push_back(CacheKey::Create("missing"sv, IoHash::Zero));
+ }
+
+ GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ size_t KeyIndex = 0;
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ const bool Missing = Index++ % 2 != 0;
+
+ if (Missing)
+ {
+ CHECK(!Record);
+ }
+ else
+ {
+ const CacheKey& ExpectedKey = ExistingKeys[KeyIndex++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ CHECK(Value.Body);
+ }
+ }
+ }
+ }
+
+ SUBCASE("policy - 'QueryLocal' does not query upstream")
+ {
+ using namespace utils;
+
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamServer(TestEnv);
+ SpawnServer(UpstreamServer, UpstreamCfg);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4);
+
+ CachePolicy Policy = CachePolicy::QueryLocal;
+ GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(!Record);
+ }
+ }
+
+ SUBCASE("policy - 'QueryRemote' does query upstream")
+ {
+ using namespace utils;
+
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamServer(TestEnv);
+ SpawnServer(UpstreamServer, UpstreamCfg);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4);
+
+ CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
+ GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ }
+ }
+
+ SUBCASE("policy - 'QueryLocal' on put allows overwrite with differing value when not limiting overwrites")
+ {
+ using namespace utils;
+
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamServer(TestEnv);
+ SpawnServer(UpstreamServer, UpstreamCfg);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ size_t PayloadSize = 1024;
+ std::string_view Namespace("ue4.ddc"sv);
+ std::string_view Bucket("mastodon"sv);
+ const size_t NumRecords = 4;
+ std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
+
+ for (const zen::CacheKey& CacheKey : Keys)
+ {
+ cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
+ AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default);
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+ HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ cacherequests::PutCacheRecordsResult ParsedResult;
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ CHECK(!Response.IsNull());
+ CHECK(ParsedResult.Parse(Response));
+ for (bool ResponseSuccess : ParsedResult.Success)
+ {
+ CHECK(ResponseSuccess);
+ }
+ CHECK(ParsedResult.Details.empty());
+ }
+
+ auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
+ CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
+ GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ CHECK(Value.RawSize == PayloadSize * 2);
+ }
+ }
+ };
+
+ // Check that the records are present and overwritten in the local server
+ CheckRecordCorrectness(LocalCfg);
+
+ // Check that the records are present and overwritten in the upstream server
+ CheckRecordCorrectness(UpstreamCfg);
+ }
+
+ SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites")
+ {
+ using namespace utils;
+
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamServer(TestEnv);
+ SpawnServer(UpstreamServer, UpstreamCfg);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites");
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ size_t PayloadSize = 1024;
+ std::string_view Namespace("ue4.ddc"sv);
+ std::string_view Bucket("mastodon"sv);
+ const size_t NumRecords = 4;
+ std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
+
+ for (const zen::CacheKey& CacheKey : Keys)
+ {
+ cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
+ AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default);
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+
+ HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ cacherequests::PutCacheRecordsResult ParsedResult;
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ CHECK(!Response.IsNull());
+ CHECK(ParsedResult.Parse(Response));
+ CHECK(Request.Requests.size() == ParsedResult.Success.size());
+ for (bool ResponseSuccess : ParsedResult.Success)
+ {
+ CHECK(ResponseSuccess);
+ }
+ CHECK(Request.Requests.size() == ParsedResult.Details.size());
+ for (const CbObjectView& Details : ParsedResult.Details)
+ {
+ CHECK(Details);
+ CHECK(Details["RawHash"sv].IsHash());
+ CHECK(Details["RawSize"sv].IsInteger());
+ CHECK(Details["Record"sv].IsObject());
+ }
+ }
+
+ auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
+ CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
+ GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ CHECK(Value.RawSize == PayloadSize);
+ }
+ }
+ };
+
+ // Check that the records are present and not overwritten in the local server
+ CheckRecordCorrectness(LocalCfg);
+
+ // Check that the records are present and not overwritten in the upstream server
+ CheckRecordCorrectness(UpstreamCfg);
+ }
+
+ SUBCASE("policy - no 'QueryLocal' on put allows overwrite with differing value when limiting overwrites")
+ {
+ using namespace utils;
+
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamServer(TestEnv);
+ SpawnServer(UpstreamServer, UpstreamCfg);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites");
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
+
+ size_t PayloadSize = 1024;
+ std::string_view Namespace("ue4.ddc"sv);
+ std::string_view Bucket("mastodon"sv);
+ const size_t NumRecords = 4;
+ std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize);
+
+ for (const zen::CacheKey& CacheKey : Keys)
+ {
+ cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
+ AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Store);
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+ HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ cacherequests::PutCacheRecordsResult ParsedResult;
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ CHECK(!Response.IsNull());
+ CHECK(ParsedResult.Parse(Response));
+ for (bool ResponseSuccess : ParsedResult.Success)
+ {
+ CHECK(ResponseSuccess);
+ }
+ CHECK(ParsedResult.Details.empty());
+ }
+
+ auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
+ CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
+ GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ CHECK(Value.RawSize == PayloadSize * 2);
+ }
+ }
+ };
+
+ // Check that the records are present and overwritten in the local server
+ CheckRecordCorrectness(LocalCfg);
+
+ // Check that the records are present and overwritten in the upstream server
+ CheckRecordCorrectness(UpstreamCfg);
+ }
+
+ SUBCASE("policy - 'QueryLocal' on put allows overwrite with equivalent value when limiting overwrites")
+ {
+ using namespace utils;
+
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamServer(TestEnv);
+ SpawnServer(UpstreamServer, UpstreamCfg);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites");
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ HttpClient LocalHttp{LocalCfg.BaseUri};
+ HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
+
+ size_t PayloadSize = 1024;
+ std::string_view Namespace("ue4.ddc"sv);
+ std::string_view Bucket("mastodon"sv);
+ const size_t NumRecords = 4;
+ std::vector<CbPackage> Packages;
+ std::vector<zen::CacheKey> Keys =
+ PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1, CachePolicy::Default, &Packages);
+
+ for (const CbPackage& Package : Packages)
+ {
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+ HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+ cacherequests::PutCacheRecordsResult ParsedResult;
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ CHECK(!Response.IsNull());
+ CHECK(ParsedResult.Parse(Response));
+ for (bool ResponseSuccess : ParsedResult.Success)
+ {
+ CHECK(ResponseSuccess);
+ }
+ CHECK(ParsedResult.Details.empty());
+ }
+
+ auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
+ CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
+ GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ CHECK(Value.RawSize == PayloadSize);
+ }
+ }
+ };
+
+ // Check that the records are present and unchanged in the local server
+ CheckRecordCorrectness(LocalCfg);
+
+ // Check that the records are present and unchanged in the upstream server
+ CheckRecordCorrectness(UpstreamCfg);
+ }
+
+ // TODO: Propagation for rejected PUTs
+ // SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites but allows propagation to
+ // upstream")
+ // {
+ // using namespace utils;
+
+ // ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ // ZenServerInstance UpstreamServer(TestEnv);
+ // SpawnServer(UpstreamServer, UpstreamCfg);
+
+ // ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port,
+ // "--cache-bucket-limit-overwrites"); ZenServerInstance LocalServer(TestEnv); SpawnServer(LocalServer, LocalCfg);
+
+ // size_t PayloadSize = 1024;
+ // std::string_view Namespace("ue4.ddc"sv);
+ // std::string_view Bucket("mastodon"sv);
+ // const size_t NumRecords = 4;
+ // std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1,
+ // CachePolicy::Local);
+
+ // for (const zen::CacheKey& CacheKey : Keys)
+ // {
+ // cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
+ // AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default);
+
+ // CbPackage Package;
+ // CHECK(Request.Format(Package));
+
+ // IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ // cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)},
+ // cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}},
+ // cpr::Body{(const char*)Body.GetData(), Body.GetSize()});
+
+ // CHECK(Result.status_code == 200);
+ // cacherequests::PutCacheRecordsResult ParsedResult;
+ // CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size()));
+ // CHECK(!Response.IsNull());
+ // CHECK(ParsedResult.Parse(Response));
+ // for (bool ResponseSuccess : ParsedResult.Success)
+ // {
+ // CHECK(!ResponseSuccess);
+ // }
+ // }
+
+ // auto CheckRecordCorrectness = [&](const ZenConfig& Cfg, size_t ExpectedPayloadSize) {
+ // CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
+ // GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
+
+ // CHECK(Result.Result.Results.size() == Keys.size());
+
+ // for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ // {
+ // CHECK(Record);
+ // const CacheKey& ExpectedKey = Keys[Index++];
+ // CHECK(Record->Key == ExpectedKey);
+ // for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ // {
+ // CHECK(Value.RawSize == ExpectedPayloadSize);
+ // }
+ // }
+ // };
+
+ // // Check that the records are present and not overwritten in the local server
+ // CheckRecordCorrectness(LocalCfg, PayloadSize);
+
+ // // Check that the records are present and are the newer size in the upstream server
+ // CheckRecordCorrectness(UpstreamCfg, PayloadSize*2);
+ // }
+
+ SUBCASE("RpcAcceptOptions")
+ {
+ using namespace utils;
+
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance Inst(TestEnv);
+ Inst.SetTestDir(TestDir);
+
+ const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady();
+ const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort);
+
+ std::vector<zen::CacheKey> SmallKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 4, 1024);
+ std::vector<zen::CacheKey> LargeKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 4, 1024 * 1024 * 16, SmallKeys.size());
+
+ std::vector<zen::CacheKey> Keys(SmallKeys.begin(), SmallKeys.end());
+ Keys.insert(Keys.end(), LargeKeys.begin(), LargeKeys.end());
+
+ {
+ GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
+ IoBufferFileReference Ref;
+ bool IsFileRef = Body.GetFileReference(Ref);
+ CHECK(!IsFileRef);
+ }
+ }
+ }
+
+ // File path, but only for large files
+ {
+ GetCacheRecordResult Result =
+ GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default, RpcAcceptOptions::kAllowLocalReferences);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
+ IoBufferFileReference Ref;
+ bool IsFileRef = Body.GetFileReference(Ref);
+ CHECK(IsFileRef == (Body.Size() > 1024));
+ }
+ }
+ }
+
+ // File path, for all files
+ {
+ GetCacheRecordResult Result =
+ GetCacheRecords(BaseUri,
+ "ue4.ddc"sv,
+ Keys,
+ CachePolicy::Default,
+ RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences);
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
+ IoBufferFileReference Ref;
+ bool IsFileRef = Body.GetFileReference(Ref);
+ CHECK(IsFileRef);
+ }
+ }
+ }
+
+ // File handle, but only for large files
+ {
+ GetCacheRecordResult Result = GetCacheRecords(BaseUri,
+ "ue4.ddc"sv,
+ Keys,
+ CachePolicy::Default,
+ RpcAcceptOptions::kAllowLocalReferences,
+ GetCurrentProcessId());
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
+ IoBufferFileReference Ref;
+ bool IsFileRef = Body.GetFileReference(Ref);
+ CHECK(IsFileRef == (Body.Size() > 1024));
+ }
+ }
+ }
+
+ // File handle, for all files
+ {
+ GetCacheRecordResult Result =
+ GetCacheRecords(BaseUri,
+ "ue4.ddc"sv,
+ Keys,
+ CachePolicy::Default,
+ RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences,
+ GetCurrentProcessId());
+
+ CHECK(Result.Result.Results.size() == Keys.size());
+
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ CHECK(Record);
+ const CacheKey& ExpectedKey = Keys[Index++];
+ CHECK(Record->Key == ExpectedKey);
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
+ IoBufferFileReference Ref;
+ bool IsFileRef = Body.GetFileReference(Ref);
+ CHECK(IsFileRef);
+ }
+ }
+ }
+ }
+}
+
+TEST_CASE("zcache.failing.upstream")
+{
+ // This is an exploratory test that takes a long time to run, so lets skip it by default
+ if (true)
+ {
+ return;
+ }
+
+ using namespace std::literals;
+ using namespace utils;
+
+ ZenConfig Upstream1Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance Upstream1Server(TestEnv);
+ SpawnServer(Upstream1Server, Upstream1Cfg);
+
+ ZenConfig Upstream2Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance Upstream2Server(TestEnv);
+ SpawnServer(Upstream2Server, Upstream2Cfg);
+
+ std::vector<std::uint16_t> UpstreamPorts = {Upstream1Cfg.Port, Upstream2Cfg.Port};
+ ZenConfig LocalCfg = ZenConfig::NewWithThreadedUpstreams(TestEnv.GetNewPortNumber(), UpstreamPorts, false);
+ LocalCfg.Args += (" --upstream-thread-count 2");
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ const uint16_t LocalPortNumber = LocalCfg.Port;
+ const auto LocalUri = fmt::format("http://localhost:{}/z$", LocalPortNumber);
+ const auto Upstream1Uri = fmt::format("http://localhost:{}/z$", Upstream1Cfg.Port);
+ const auto Upstream2Uri = fmt::format("http://localhost:{}/z$", Upstream2Cfg.Port);
+
+ bool Upstream1Running = true;
+ bool Upstream2Running = true;
+
+ using namespace std::literals;
+
+ auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request,
+ const zen::CacheKey& CacheKey,
+ size_t PayloadSize,
+ CachePolicy RecordPolicy) {
+ std::vector<uint32_t> Data;
+ Data.resize(PayloadSize / 4);
+ for (uint32_t Idx = 0; Idx < PayloadSize / 4; ++Idx)
+ {
+ Data[Idx] = (*reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0])) + Idx;
+ }
+
+ CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size() * 4));
+ Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy});
+ };
+
+ auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri,
+ std::string_view Namespace,
+ std::string_view Bucket,
+ size_t Num,
+ size_t KeyOffset,
+ size_t PayloadSize = 8192) -> std::vector<CacheKey> {
+ std::vector<zen::CacheKey> OutKeys;
+
+ cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
+ for (size_t Key = 1; Key <= Num; ++Key)
+ {
+ zen::IoHash KeyHash;
+ ((size_t*)(KeyHash.Hash))[0] = KeyOffset + Key;
+ const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash);
+
+ AppendCacheRecord(Request, CacheKey, PayloadSize, CachePolicy::Default);
+ OutKeys.push_back(CacheKey);
+ }
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+
+ HttpClient Http{BaseUri};
+
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ if (Result.StatusCode != HttpResponseCode::OK)
+ {
+ ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage(""));
+ OutKeys.clear();
+ }
+
+ return OutKeys;
+ };
+
+ struct GetCacheRecordResult
+ {
+ zen::CbPackage Response;
+ cacherequests::GetCacheRecordsResult Result;
+ bool Success = false;
+ };
+
+ auto GetCacheRecords = [](std::string_view BaseUri,
+ std::string_view Namespace,
+ std::span<zen::CacheKey> Keys,
+ zen::CachePolicy Policy) -> GetCacheRecordResult {
+ cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
+ .DefaultPolicy = Policy,
+ .Namespace = std::string(Namespace)};
+ for (const CacheKey& Key : Keys)
+ {
+ Request.Requests.push_back({.Key = Key});
+ }
+
+ CbObjectWriter RequestWriter;
+ CHECK(Request.Format(RequestWriter));
+
+ IoBuffer Body = RequestWriter.Save().GetBuffer().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbObject);
+
+ HttpClient Http{BaseUri};
+
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ GetCacheRecordResult OutResult;
+
+ if (Result.StatusCode == HttpResponseCode::OK)
+ {
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ if (!Response.IsNull())
+ {
+ OutResult.Response = std::move(Response);
+ CHECK(OutResult.Result.Parse(OutResult.Response));
+ OutResult.Success = true;
+ }
+ }
+ else
+ {
+ ZEN_DEBUG("GetCacheRecords with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage(""));
+ }
+
+ return OutResult;
+ };
+
+ // Populate with some simple data
+
+ CachePolicy Policy = CachePolicy::Default;
+
+ const size_t ThreadCount = 128;
+ const size_t KeyMultiplier = 16384;
+ const size_t RecordsPerRequest = 64;
+ WorkerThreadPool Pool(ThreadCount);
+
+ std::atomic_size_t Completed = 0;
+
+ auto Keys = new std::vector<CacheKey>[ThreadCount * KeyMultiplier];
+ RwLock KeysLock;
+
+ for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++)
+ {
+ size_t Iteration = I;
+ Pool.ScheduleWork(
+ [&] {
+ std::vector<CacheKey> NewKeys =
+ PutCacheRecords(LocalUri, "ue4.ddc"sv, "mastodon"sv, RecordsPerRequest, I * RecordsPerRequest);
+ if (NewKeys.size() != RecordsPerRequest)
+ {
+ ZEN_DEBUG("PutCacheRecords iteration {} failed", Iteration);
+ Completed.fetch_add(1);
+ return;
+ }
+ {
+ RwLock::ExclusiveLockScope _(KeysLock);
+ Keys[Iteration].swap(NewKeys);
+ }
+ Completed.fetch_add(1);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ bool UseUpstream1 = false;
+ while (Completed < ThreadCount * KeyMultiplier)
+ {
+ Sleep(8000);
+
+ if (UseUpstream1)
+ {
+ if (Upstream2Running)
+ {
+ Upstream2Server.EnableTermination();
+ Upstream2Server.Shutdown();
+ Sleep(100);
+ Upstream2Running = false;
+ }
+ if (!Upstream1Running)
+ {
+ SpawnServer(Upstream1Server, Upstream1Cfg);
+ Upstream1Running = true;
+ }
+ UseUpstream1 = !UseUpstream1;
+ }
+ else
+ {
+ if (Upstream1Running)
+ {
+ Upstream1Server.EnableTermination();
+ Upstream1Server.Shutdown();
+ Sleep(100);
+ Upstream1Running = false;
+ }
+ if (!Upstream2Running)
+ {
+ SpawnServer(Upstream2Server, Upstream2Cfg);
+ Upstream2Running = true;
+ }
+ UseUpstream1 = !UseUpstream1;
+ }
+ }
+
+ Completed = 0;
+ for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++)
+ {
+ size_t Iteration = I;
+ std::vector<CacheKey>& LocalKeys = Keys[Iteration];
+ if (LocalKeys.empty())
+ {
+ Completed.fetch_add(1);
+ continue;
+ }
+ Pool.ScheduleWork(
+ [&] {
+ GetCacheRecordResult Result = GetCacheRecords(LocalUri, "ue4.ddc"sv, LocalKeys, Policy);
+
+ if (!Result.Success)
+ {
+ ZEN_DEBUG("GetCacheRecords iteration {} failed", Iteration);
+ Completed.fetch_add(1);
+ return;
+ }
+
+ if (Result.Result.Results.size() != LocalKeys.size())
+ {
+ ZEN_DEBUG("GetCacheRecords iteration {} empty records", Iteration);
+ Completed.fetch_add(1);
+ return;
+ }
+ for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
+ {
+ const CacheKey& ExpectedKey = LocalKeys[Index++];
+ if (!Record)
+ {
+ continue;
+ }
+ if (Record->Key != ExpectedKey)
+ {
+ continue;
+ }
+ if (Record->Values.size() != 1)
+ {
+ continue;
+ }
+
+ for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
+ {
+ if (!Value.Body)
+ {
+ continue;
+ }
+ }
+ }
+ Completed.fetch_add(1);
+ },
+ WorkerThreadPool::EMode::EnableBacklog);
+ }
+ while (Completed < ThreadCount * KeyMultiplier)
+ {
+ Sleep(10);
+ }
+}
+
+TEST_CASE("zcache.rpc.partialchunks")
+{
+ using namespace std::literals;
+ using namespace utils;
+
+ ZenConfig LocalCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance Server(TestEnv);
+ SpawnServer(Server, LocalCfg);
+
+ std::vector<CompressedBuffer> Attachments;
+
+ const auto BaseUri = fmt::format("http://localhost:{}/z$", Server.GetBasePort());
+
+ auto GenerateKey = [](std::string_view Bucket, size_t KeyIndex) -> CacheKey {
+ IoHash KeyHash;
+ ((size_t*)(KeyHash.Hash))[0] = KeyIndex;
+ return CacheKey::Create(Bucket, KeyHash);
+ };
+
+ auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request,
+ const CacheKey& CacheKey,
+ size_t AttachmentCount,
+ size_t AttachmentsSize,
+ CachePolicy RecordPolicy) -> std::vector<std::pair<Oid, CompressedBuffer>> {
+ std::vector<std::pair<Oid, CompressedBuffer>> AttachmentBuffers;
+ std::vector<cacherequests::PutCacheRecordRequestValue> Attachments;
+ for (size_t AttachmentIndex = 0; AttachmentIndex < AttachmentCount; AttachmentIndex++)
+ {
+ CompressedBuffer Value = CreateSemiRandomBlob(AttachmentsSize);
+ AttachmentBuffers.push_back(std::make_pair(Oid::NewOid(), Value));
+ Attachments.push_back({.Id = AttachmentBuffers.back().first, .Body = std::move(Value)});
+ }
+ Request.Requests.push_back({.Key = CacheKey, .Values = Attachments, .Policy = RecordPolicy});
+ return AttachmentBuffers;
+ };
+
+ auto PutCacheRecords = [&AppendCacheRecord, &GenerateKey](
+ std::string_view BaseUri,
+ std::string_view Namespace,
+ std::string_view Bucket,
+ size_t KeyOffset,
+ size_t Num,
+ size_t AttachmentCount,
+ size_t AttachmentsSize =
+ 8192) -> std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> {
+ std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> Keys;
+
+ cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
+ for (size_t Key = 1; Key <= Num; ++Key)
+ {
+ const CacheKey NewCacheKey = GenerateKey(Bucket, KeyOffset + Key);
+ std::vector<std::pair<Oid, CompressedBuffer>> Attachments =
+ AppendCacheRecord(Request, NewCacheKey, AttachmentCount, AttachmentsSize, CachePolicy::Default);
+ Keys.push_back(std::make_pair(NewCacheKey, std::move(Attachments)));
+ }
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+
+ HttpClient Http{BaseUri};
+
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ if (Result.StatusCode != HttpResponseCode::OK)
+ {
+ ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage(""));
+ Keys.clear();
+ }
+
+ return Keys;
+ };
+
+ std::string_view TestBucket = "partialcachevaluetests"sv;
+ std::string_view TestNamespace = "ue4.ddc"sv;
+ auto RecordsWithSmallAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 0, 3, 2, 4096u);
+ CHECK(RecordsWithSmallAttachments.size() == 3);
+ auto RecordsWithLargeAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 10, 1, 2, 8u * 1024u * 1024u);
+ CHECK(RecordsWithLargeAttachments.size() == 1);
+
+ struct PartialOptions
+ {
+ uint64_t Offset = 0ull;
+ uint64_t Size = ~0ull;
+ RpcAcceptOptions AcceptOptions = RpcAcceptOptions::kNone;
+ };
+
+ auto GetCacheChunk = [](std::string_view BaseUri,
+ std::string_view Namespace,
+ const CacheKey& Key,
+ const Oid& ValueId,
+ const PartialOptions& Options = {}) -> cacherequests::GetCacheChunksResult {
+ cacherequests::GetCacheChunksRequest Request = {
+ .AcceptMagic = kCbPkgMagic,
+ .AcceptOptions = (uint16_t)Options.AcceptOptions,
+ .Namespace = std::string(Namespace),
+ .Requests = {{.Key = Key, .ValueId = ValueId, .RawOffset = Options.Offset, .RawSize = Options.Size}}};
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+
+ HttpClient Http{BaseUri};
+
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+
+ CHECK(Result.StatusCode == HttpResponseCode::OK);
+
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ bool Loaded = !Response.IsNull();
+ CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load.");
+ cacherequests::GetCacheChunksResult GetCacheChunksResult;
+ CHECK(GetCacheChunksResult.Parse(Response));
+ return GetCacheChunksResult;
+ };
+
+ auto GetAndVerifyChunk = [&GetCacheChunk](std::string_view BaseUri,
+ std::string_view Namespace,
+ const CacheKey& Key,
+ const Oid& ChunkId,
+ const CompressedBuffer& VerifyData,
+ const PartialOptions& Options = {}) {
+ cacherequests::GetCacheChunksResult Result = GetCacheChunk(BaseUri, Namespace, Key, ChunkId, Options);
+ CHECK(Result.Results.size() == 1);
+ bool CanGetPartial = ((uint16_t)Options.AcceptOptions & (uint16_t)RpcAcceptOptions::kAllowPartialCacheChunks);
+ if (!CanGetPartial)
+ {
+ CHECK(Result.Results[0].FragmentOffset == 0);
+ CHECK(Result.Results[0].Body.GetCompressedSize() == VerifyData.GetCompressedSize());
+ }
+ IoBuffer SourceDecompressed = VerifyData.Decompress(Options.Offset, Options.Size).AsIoBuffer();
+ IoBuffer ReceivedDecompressed =
+ Result.Results[0].Body.Decompress(Options.Offset - Result.Results[0].FragmentOffset, Options.Size).AsIoBuffer();
+ CHECK(SourceDecompressed.GetView().EqualBytes(ReceivedDecompressed.GetView()));
+ };
+
+ GetAndVerifyChunk(BaseUri,
+ TestNamespace,
+ RecordsWithSmallAttachments[0].first,
+ RecordsWithSmallAttachments[0].second[0].first,
+ RecordsWithSmallAttachments[0].second[0].second);
+ GetAndVerifyChunk(BaseUri,
+ TestNamespace,
+ RecordsWithSmallAttachments[0].first,
+ RecordsWithSmallAttachments[0].second[0].first,
+ RecordsWithSmallAttachments[0].second[0].second,
+ PartialOptions{.Offset = 378, .Size = 519, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences});
+ GetAndVerifyChunk(
+ BaseUri,
+ TestNamespace,
+ RecordsWithSmallAttachments[0].first,
+ RecordsWithSmallAttachments[0].second[0].first,
+ RecordsWithSmallAttachments[0].second[0].second,
+ PartialOptions{.Offset = 378,
+ .Size = 519,
+ .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks});
+ GetAndVerifyChunk(BaseUri,
+ TestNamespace,
+ RecordsWithLargeAttachments[0].first,
+ RecordsWithLargeAttachments[0].second[0].first,
+ RecordsWithLargeAttachments[0].second[0].second,
+ PartialOptions{.AcceptOptions = RpcAcceptOptions::kAllowLocalReferences});
+ GetAndVerifyChunk(BaseUri,
+ TestNamespace,
+ RecordsWithLargeAttachments[0].first,
+ RecordsWithLargeAttachments[0].second[0].first,
+ RecordsWithLargeAttachments[0].second[0].second,
+ PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u});
+ GetAndVerifyChunk(
+ BaseUri,
+ TestNamespace,
+ RecordsWithLargeAttachments[0].first,
+ RecordsWithLargeAttachments[0].second[0].first,
+ RecordsWithLargeAttachments[0].second[0].second,
+ PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences});
+ GetAndVerifyChunk(
+ BaseUri,
+ TestNamespace,
+ RecordsWithLargeAttachments[0].first,
+ RecordsWithLargeAttachments[0].second[0].first,
+ RecordsWithLargeAttachments[0].second[0].second,
+ PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowPartialCacheChunks});
+ GetAndVerifyChunk(
+ BaseUri,
+ TestNamespace,
+ RecordsWithLargeAttachments[0].first,
+ RecordsWithLargeAttachments[0].second[0].first,
+ RecordsWithLargeAttachments[0].second[0].second,
+ PartialOptions{.Offset = 1024u * 1024u,
+ .Size = 512u * 1024u,
+ .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks});
+ GetAndVerifyChunk(
+ BaseUri,
+ TestNamespace,
+ RecordsWithLargeAttachments[0].first,
+ RecordsWithLargeAttachments[0].second[0].first,
+ RecordsWithLargeAttachments[0].second[0].second,
+ PartialOptions{.Offset = 1024u * 1024u,
+ .Size = 512u * 1024u,
+ .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences |
+ RpcAcceptOptions::kAllowPartialCacheChunks});
+}
+
+IoBuffer
+FormatPackageBody(const CbPackage& Package)
+{
+ IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
+ Body.SetContentType(HttpContentType::kCbPackage);
+ return Body;
+}
+
+TEST_CASE("zcache.rpc.allpolicies")
+{
+ using namespace std::literals;
+ using namespace utils;
+
+ ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
+ ZenServerInstance UpstreamServer(TestEnv);
+ SpawnServer(UpstreamServer, UpstreamCfg);
+
+ ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
+ ZenServerInstance LocalServer(TestEnv);
+ SpawnServer(LocalServer, LocalCfg);
+
+ const auto BaseUri = fmt::format("http://localhost:{}/z$", LocalServer.GetBasePort());
+ HttpClient Http{BaseUri};
+
+ std::string_view TestVersion = "F72150A02AE34B57A9EC91D36BA1CE08"sv;
+ std::string_view TestBucket = "allpoliciestest"sv;
+ std::string_view TestNamespace = "ue4.ddc"sv;
+
+ // NumKeys = (2 Value vs Record)*(2 SkipData vs Default)*(2 ForceMiss vs Not)*(2 use local)
+ // *(2 use remote)*(2 UseValue Policy vs not)*(4 cases per type)
+ constexpr int NumKeys = 256;
+ constexpr int NumValues = 4;
+ Oid ValueIds[NumValues];
+ IoHash Hash;
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ ExtendableStringBuilder<16> ValueName;
+ ValueName << "ValueId_"sv << ValueIndex;
+ static_assert(sizeof(IoHash) >= sizeof(Oid));
+ ValueIds[ValueIndex] = Oid::FromMemory(IoHash::HashBuffer(ValueName.Data(), ValueName.Size() * sizeof(ValueName.Data()[0])).Hash);
+ }
+
+ struct KeyData;
+ struct UserData
+ {
+ UserData& Set(KeyData* InKeyData, int InValueIndex)
+ {
+ Data = InKeyData;
+ ValueIndex = InValueIndex;
+ return *this;
+ }
+ KeyData* Data = nullptr;
+ int ValueIndex = 0;
+ };
+ struct KeyData
+ {
+ CompressedBuffer BufferValues[NumValues];
+ uint64_t IntValues[NumValues];
+ UserData ValueUserData[NumValues];
+ bool ReceivedChunk[NumValues];
+ CacheKey Key;
+ UserData KeyUserData;
+ uint32_t KeyIndex = 0;
+ bool GetRequestsData = true;
+ bool UseValueAPI = false;
+ bool UseValuePolicy = false;
+ bool ForceMiss = false;
+ bool UseLocal = true;
+ bool UseRemote = true;
+ bool ShouldBeHit = true;
+ bool ReceivedPut = false;
+ bool ReceivedGet = false;
+ bool ReceivedPutValue = false;
+ bool ReceivedGetValue = false;
+ };
+ struct CachePutRequest
+ {
+ CacheKey Key;
+ CbObject Record;
+ CacheRecordPolicy Policy;
+ KeyData* Values;
+ UserData* Data;
+ };
+ struct CachePutValueRequest
+ {
+ CacheKey Key;
+ CompressedBuffer Value;
+ CachePolicy Policy;
+ UserData* Data;
+ };
+ struct CacheGetRequest
+ {
+ CacheKey Key;
+ CacheRecordPolicy Policy;
+ UserData* Data;
+ };
+ struct CacheGetValueRequest
+ {
+ CacheKey Key;
+ CachePolicy Policy;
+ UserData* Data;
+ };
+ struct CacheGetChunkRequest
+ {
+ CacheKey Key;
+ Oid ValueId;
+ uint64_t RawOffset;
+ uint64_t RawSize;
+ IoHash RawHash;
+ CachePolicy Policy;
+ UserData* Data;
+ };
+
+ KeyData KeyDatas[NumKeys];
+ std::vector<CachePutRequest> PutRequests;
+ std::vector<CachePutValueRequest> PutValueRequests;
+ std::vector<CacheGetRequest> GetRequests;
+ std::vector<CacheGetValueRequest> GetValueRequests;
+ std::vector<CacheGetChunkRequest> ChunkRequests;
+
+ for (uint32_t KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex)
+ {
+ IoHashStream KeyWriter;
+ KeyWriter.Append(TestVersion.data(), TestVersion.length() * sizeof(TestVersion.data()[0]));
+ KeyWriter.Append(&KeyIndex, sizeof(KeyIndex));
+ IoHash KeyHash = KeyWriter.GetHash();
+ KeyData& KeyData = KeyDatas[KeyIndex];
+
+ KeyData.Key = CacheKey::Create(TestBucket, KeyHash);
+ KeyData.KeyIndex = KeyIndex;
+ KeyData.GetRequestsData = (KeyIndex & (1 << 1)) == 0;
+ KeyData.UseValueAPI = (KeyIndex & (1 << 2)) != 0;
+ KeyData.UseValuePolicy = (KeyIndex & (1 << 3)) != 0;
+ KeyData.ForceMiss = (KeyIndex & (1 << 4)) == 0;
+ KeyData.UseLocal = (KeyIndex & (1 << 5)) == 0;
+ KeyData.UseRemote = (KeyIndex & (1 << 6)) == 0;
+ KeyData.ShouldBeHit = !KeyData.ForceMiss && (KeyData.UseLocal || KeyData.UseRemote);
+ CachePolicy SharedPolicy = KeyData.UseLocal ? CachePolicy::Local : CachePolicy::None;
+ SharedPolicy |= KeyData.UseRemote ? CachePolicy::Remote : CachePolicy::None;
+ CachePolicy PutPolicy = SharedPolicy;
+ CachePolicy GetPolicy = SharedPolicy;
+ GetPolicy |= !KeyData.GetRequestsData ? CachePolicy::SkipData : CachePolicy::None;
+ CacheKey& Key = KeyData.Key;
+
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ KeyData.IntValues[ValueIndex] = static_cast<uint64_t>(KeyIndex) | (static_cast<uint64_t>(ValueIndex) << 32);
+ KeyData.BufferValues[ValueIndex] =
+ CompressedBuffer::Compress(SharedBuffer::MakeView(&KeyData.IntValues[ValueIndex], sizeof(KeyData.IntValues[ValueIndex])));
+ KeyData.ReceivedChunk[ValueIndex] = false;
+ }
+
+ UserData& KeyUserData = KeyData.KeyUserData.Set(&KeyData, -1);
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ KeyData.ValueUserData[ValueIndex].Set(&KeyData, ValueIndex);
+ }
+ if (!KeyData.UseValueAPI)
+ {
+ CbObjectWriter Builder;
+ Builder.BeginObject("key"sv);
+ Builder << "Bucket"sv << Key.Bucket << "Hash"sv << Key.Hash;
+ Builder.EndObject();
+ Builder.BeginArray("Values"sv);
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ Builder.BeginObject();
+ Builder.AddObjectId("Id"sv, ValueIds[ValueIndex]);
+ Builder.AddBinaryAttachment("RawHash"sv, KeyData.BufferValues[ValueIndex].DecodeRawHash());
+ Builder.AddInteger("RawSize"sv, KeyData.BufferValues[ValueIndex].DecodeRawSize());
+ Builder.EndObject();
+ }
+ Builder.EndArray();
+
+ CacheRecordPolicy PutRecordPolicy;
+ CacheRecordPolicy GetRecordPolicy;
+ if (!KeyData.UseValuePolicy)
+ {
+ PutRecordPolicy = CacheRecordPolicy(PutPolicy);
+ GetRecordPolicy = CacheRecordPolicy(GetPolicy);
+ }
+ else
+ {
+ // Switch the SkipData field in the Record policy so that if the CacheStore ignores the ValuePolicies
+ // it will use the wrong value for SkipData and fail our tests.
+ CacheRecordPolicyBuilder PutBuilder(PutPolicy ^ CachePolicy::SkipData);
+ CacheRecordPolicyBuilder GetBuilder(GetPolicy ^ CachePolicy::SkipData);
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ PutBuilder.AddValuePolicy(ValueIds[ValueIndex], PutPolicy);
+ GetBuilder.AddValuePolicy(ValueIds[ValueIndex], GetPolicy);
+ }
+ PutRecordPolicy = PutBuilder.Build();
+ GetRecordPolicy = GetBuilder.Build();
+ }
+ if (!KeyData.ForceMiss)
+ {
+ PutRequests.push_back({Key, Builder.Save(), PutRecordPolicy, &KeyData, &KeyUserData});
+ }
+ GetRequests.push_back({Key, GetRecordPolicy, &KeyUserData});
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ UserData& ValueUserData = KeyData.ValueUserData[ValueIndex];
+ ChunkRequests.push_back({Key, ValueIds[ValueIndex], 0, UINT64_MAX, IoHash(), GetPolicy, &ValueUserData});
+ }
+ }
+ else
+ {
+ if (!KeyData.ForceMiss)
+ {
+ PutValueRequests.push_back({Key, KeyData.BufferValues[0], PutPolicy, &KeyUserData});
+ }
+ GetValueRequests.push_back({Key, GetPolicy, &KeyUserData});
+ ChunkRequests.push_back({Key, Oid::Zero, 0, UINT64_MAX, IoHash(), GetPolicy, &KeyUserData});
+ }
+ }
+
+ // PutCacheRecords
+ {
+ CachePolicy BatchDefaultPolicy = CachePolicy::Default;
+ cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
+ .DefaultPolicy = BatchDefaultPolicy,
+ .Namespace = std::string(TestNamespace)};
+ Request.Requests.reserve(PutRequests.size());
+ for (CachePutRequest& PutRequest : PutRequests)
+ {
+ cacherequests::PutCacheRecordRequest& RecordRequest = Request.Requests.emplace_back();
+ RecordRequest.Key = PutRequest.Key;
+ RecordRequest.Policy = PutRequest.Policy;
+ RecordRequest.Values.reserve(NumValues);
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ RecordRequest.Values.push_back({.Id = ValueIds[ValueIndex], .Body = PutRequest.Values->BufferValues[ValueIndex]});
+ }
+ PutRequest.Data->Data->ReceivedPut = true;
+ }
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+ IoBuffer Body = FormatPackageBody(Package);
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "PutCacheRecords unexpectedly failed.");
+ }
+
+ // PutCacheValues
+ {
+ CachePolicy BatchDefaultPolicy = CachePolicy::Default;
+
+ cacherequests::PutCacheValuesRequest Request = {.AcceptMagic = kCbPkgMagic,
+ .DefaultPolicy = BatchDefaultPolicy,
+ .Namespace = std::string(TestNamespace)};
+ Request.Requests.reserve(PutValueRequests.size());
+ for (CachePutValueRequest& PutRequest : PutValueRequests)
+ {
+ Request.Requests.push_back({.Key = PutRequest.Key, .Body = PutRequest.Value, .Policy = PutRequest.Policy});
+ PutRequest.Data->Data->ReceivedPutValue = true;
+ }
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+
+ IoBuffer Body = FormatPackageBody(Package);
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "PutCacheValues unexpectedly failed.");
+ }
+
+ for (KeyData& KeyData : KeyDatas)
+ {
+ if (!KeyData.ForceMiss)
+ {
+ if (!KeyData.UseValueAPI)
+ {
+ CHECK_MESSAGE(KeyData.ReceivedPut, WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put.").c_str());
+ }
+ else
+ {
+ CHECK_MESSAGE(KeyData.ReceivedPutValue,
+ WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put to ValueAPI.").c_str());
+ }
+ }
+ }
+
+ // GetCacheRecords
+ {
+ CachePolicy BatchDefaultPolicy = CachePolicy::Default;
+ cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
+ .DefaultPolicy = BatchDefaultPolicy,
+ .Namespace = std::string(TestNamespace)};
+ Request.Requests.reserve(GetRequests.size());
+ for (CacheGetRequest& GetRequest : GetRequests)
+ {
+ Request.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy});
+ }
+
+ CbPackage Package;
+ CHECK(Request.Format(Package));
+ IoBuffer Body = FormatPackageBody(Package);
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheRecords unexpectedly failed.");
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ bool Loaded = !Response.IsNull();
+ CHECK_MESSAGE(Loaded, "GetCacheRecords response failed to load.");
+ cacherequests::GetCacheRecordsResult RequestResult;
+ CHECK(RequestResult.Parse(Response));
+ CHECK_MESSAGE(RequestResult.Results.size() == GetRequests.size(), "GetCacheRecords response count did not match request count.");
+ for (int Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& RecordResult : RequestResult.Results)
+ {
+ bool Succeeded = RecordResult.has_value();
+ CacheGetRequest& GetRequest = GetRequests[Index++];
+ KeyData* KeyData = GetRequest.Data->Data;
+ KeyData->ReceivedGet = true;
+ WriteToString<32> Name("Get(", KeyData->KeyIndex, ")");
+ if (KeyData->ShouldBeHit)
+ {
+ CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str());
+ }
+ else if (KeyData->ForceMiss)
+ {
+ CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, " unexpectedly succeeded.").c_str());
+ }
+ if (!KeyData->ForceMiss && Succeeded)
+ {
+ CHECK_MESSAGE(RecordResult->Values.size() == NumValues,
+ WriteToString<32>(Name, " number of values did not match.").c_str());
+ for (const cacherequests::GetCacheRecordResultValue& Value : RecordResult->Values)
+ {
+ int ExpectedValueIndex = 0;
+ for (; ExpectedValueIndex < NumValues; ++ExpectedValueIndex)
+ {
+ if (ValueIds[ExpectedValueIndex] == Value.Id)
+ {
+ break;
+ }
+ }
+ CHECK_MESSAGE(ExpectedValueIndex < NumValues, WriteToString<32>(Name, " could not find matching ValueId.").c_str());
+
+ WriteToString<32> ValueName("Get(", KeyData->KeyIndex, ",", ExpectedValueIndex, ")");
+
+ CompressedBuffer ExpectedValue = KeyData->BufferValues[ExpectedValueIndex];
+ CHECK_MESSAGE(Value.RawHash == ExpectedValue.DecodeRawHash(),
+ WriteToString<32>(ValueName, " RawHash did not match.").c_str());
+ CHECK_MESSAGE(Value.RawSize == ExpectedValue.DecodeRawSize(),
+ WriteToString<32>(ValueName, " RawSize did not match.").c_str());
+
+ if (KeyData->GetRequestsData)
+ {
+ SharedBuffer Buffer = Value.Body.Decompress();
+ CHECK_MESSAGE(Buffer.GetSize() == Value.RawSize,
+ WriteToString<32>(ValueName, " BufferSize did not match RawSize.").c_str());
+ uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0];
+ uint64_t ExpectedIntValue = KeyData->IntValues[ExpectedValueIndex];
+ CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(ValueName, " had unexpected data.").c_str());
+ }
+ }
+ }
+ }
+ }
+
+ // GetCacheValues
+ {
+ CachePolicy BatchDefaultPolicy = CachePolicy::Default;
+
+ cacherequests::GetCacheValuesRequest GetCacheValuesRequest = {.AcceptMagic = kCbPkgMagic,
+ .DefaultPolicy = BatchDefaultPolicy,
+ .Namespace = std::string(TestNamespace)};
+ GetCacheValuesRequest.Requests.reserve(GetValueRequests.size());
+ for (CacheGetValueRequest& GetRequest : GetValueRequests)
+ {
+ GetCacheValuesRequest.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy});
+ }
+
+ CbPackage Package;
+ CHECK(GetCacheValuesRequest.Format(Package));
+
+ IoBuffer Body = FormatPackageBody(Package);
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheValues unexpectedly failed.");
+ IoBuffer MessageBuffer(Result.ResponsePayload);
+ CbPackage Response = ParsePackageMessage(MessageBuffer);
+ bool Loaded = !Response.IsNull();
+ CHECK_MESSAGE(Loaded, "GetCacheValues response failed to load.");
+ cacherequests::GetCacheValuesResult GetCacheValuesResult;
+ CHECK(GetCacheValuesResult.Parse(Response));
+ for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheValuesResult.Results)
+ {
+ bool Succeeded = ValueResult.RawHash != IoHash::Zero;
+ CacheGetValueRequest& Request = GetValueRequests[Index++];
+ KeyData* KeyData = Request.Data->Data;
+ KeyData->ReceivedGetValue = true;
+ WriteToString<32> Name("GetValue("sv, KeyData->KeyIndex, ")"sv);
+
+ if (KeyData->ShouldBeHit)
+ {
+ CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str());
+ }
+ else if (KeyData->ForceMiss)
+ {
+ CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, "unexpectedly succeeded.").c_str());
+ }
+ if (!KeyData->ForceMiss && Succeeded)
+ {
+ CompressedBuffer ExpectedValue = KeyData->BufferValues[0];
+ CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(),
+ WriteToString<32>(Name, " RawHash did not match.").c_str());
+ CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(),
+ WriteToString<32>(Name, " RawSize did not match.").c_str());
+
+ if (KeyData->GetRequestsData)
+ {
+ SharedBuffer Buffer = ValueResult.Body.Decompress();
+ CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize,
+ WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str());
+ uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0];
+ uint64_t ExpectedIntValue = KeyData->IntValues[0];
+ CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str());
+ }
+ }
+ }
+ }
+
+ // GetCacheChunks
+ {
+ std::sort(ChunkRequests.begin(), ChunkRequests.end(), [](CacheGetChunkRequest& A, CacheGetChunkRequest& B) {
+ return A.Key.Hash < B.Key.Hash;
+ });
+ CachePolicy BatchDefaultPolicy = CachePolicy::Default;
+ cacherequests::GetCacheChunksRequest GetCacheChunksRequest = {.AcceptMagic = kCbPkgMagic,
+ .DefaultPolicy = BatchDefaultPolicy,
+ .Namespace = std::string(TestNamespace)};
+ GetCacheChunksRequest.Requests.reserve(ChunkRequests.size());
+ for (CacheGetChunkRequest& ChunkRequest : ChunkRequests)
+ {
+ GetCacheChunksRequest.Requests.push_back({.Key = ChunkRequest.Key,
+ .ValueId = ChunkRequest.ValueId,
+ .ChunkId = IoHash(),
+ .RawOffset = ChunkRequest.RawOffset,
+ .RawSize = ChunkRequest.RawSize,
+ .Policy = ChunkRequest.Policy});
+ }
+ CbPackage Package;
+ CHECK(GetCacheChunksRequest.Format(Package));
+
+ IoBuffer Body = FormatPackageBody(Package);
+ HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheChunks unexpectedly failed.");
+ CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
+ bool Loaded = !Response.IsNull();
+ CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load.");
+ cacherequests::GetCacheChunksResult GetCacheChunksResult;
+ CHECK(GetCacheChunksResult.Parse(Response));
+ CHECK_MESSAGE(GetCacheChunksResult.Results.size() == ChunkRequests.size(),
+ "GetCacheChunks response count did not match request count.");
+
+ for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheChunksResult.Results)
+ {
+ bool Succeeded = ValueResult.RawHash != IoHash::Zero;
+
+ CacheGetChunkRequest& Request = ChunkRequests[Index++];
+ KeyData* KeyData = Request.Data->Data;
+ int ValueIndex = Request.Data->ValueIndex >= 0 ? Request.Data->ValueIndex : 0;
+ KeyData->ReceivedChunk[ValueIndex] = true;
+ WriteToString<32> Name("GetChunks("sv, KeyData->KeyIndex, ","sv, ValueIndex, ")"sv);
+
+ if (KeyData->ShouldBeHit)
+ {
+ CHECK_MESSAGE(Succeeded, WriteToString<256>(Name, " unexpectedly failed."sv).c_str());
+ }
+ else if (KeyData->ForceMiss)
+ {
+ CHECK_MESSAGE(!Succeeded, WriteToString<256>(Name, " unexpectedly succeeded."sv).c_str());
+ }
+ if (KeyData->ShouldBeHit && Succeeded)
+ {
+ CompressedBuffer ExpectedValue = KeyData->BufferValues[ValueIndex];
+ CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(),
+ WriteToString<32>(Name, " had unexpected RawHash.").c_str());
+ CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(),
+ WriteToString<32>(Name, " had unexpected RawSize.").c_str());
+
+ if (KeyData->GetRequestsData)
+ {
+ SharedBuffer Buffer = ValueResult.Body.Decompress();
+ CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize,
+ WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str());
+ uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0];
+ uint64_t ExpectedIntValue = KeyData->IntValues[ValueIndex];
+ CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str());
+ }
+ }
+ }
+ }
+
+ for (KeyData& KeyData : KeyDatas)
+ {
+ if (!KeyData.UseValueAPI)
+ {
+ CHECK_MESSAGE(KeyData.ReceivedGet, WriteToString<32>("Get(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str());
+ for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
+ {
+ CHECK_MESSAGE(
+ KeyData.ReceivedChunk[ValueIndex],
+ WriteToString<32>("GetChunks(", KeyData.KeyIndex, ",", ValueIndex, ") was unexpectedly not received.").c_str());
+ }
+ }
+ else
+ {
+ CHECK_MESSAGE(KeyData.ReceivedGetValue,
+ WriteToString<32>("GetValue(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str());
+ CHECK_MESSAGE(KeyData.ReceivedChunk[0],
+ WriteToString<32>("GetChunks(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str());
+ }
+ }
+}
+
+} // namespace zen::tests
+
+#endif
diff --git a/src/zenserver-test/projectstore-tests.cpp b/src/zenserver-test/projectstore-tests.cpp
new file mode 100644
index 000000000..ce3e9dcd1
--- /dev/null
+++ b/src/zenserver-test/projectstore-tests.cpp
@@ -0,0 +1,1055 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#if ZEN_WITH_TESTS
+# include "zenserver-test.h"
+# include <zencore/testing.h>
+# include <zencore/testutils.h>
+# include <zencore/workthreadpool.h>
+# include <zencore/compactbinarybuilder.h>
+# include <zencore/compactbinarypackage.h>
+# include <zencore/compress.h>
+# include <zencore/filesystem.h>
+# include <zencore/fmtutils.h>
+# include <zencore/stream.h>
+# include <zencore/string.h>
+# include <zencore/xxhash.h>
+# include <zenhttp/packageformat.h>
+# include <zenutil/zenserverprocess.h>
+# include <zenhttp/httpclient.h>
+
+# include <tsl/robin_set.h>
+# include <random>
+
+namespace zen::tests {
+
+using namespace std::literals;
+
+TEST_CASE("project.basic")
+{
+ using namespace std::literals;
+
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance Instance1(TestEnv);
+ Instance1.SetTestDir(TestDir);
+
+ const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
+
+ std::mt19937_64 mt;
+
+ zen::StringBuilder<64> BaseUri;
+ BaseUri << fmt::format("http://localhost:{}", PortNumber);
+
+ std::filesystem::path BinPath = zen::GetRunningExecutablePath();
+ std::filesystem::path RootPath = BinPath.parent_path().parent_path();
+ BinPath = BinPath.lexically_relative(RootPath);
+
+ SUBCASE("build store init")
+ {
+ {
+ HttpClient Http{BaseUri};
+
+ {
+ zen::CbObjectWriter Body;
+ Body << "id"
+ << "test";
+ Body << "root" << RootPath.c_str();
+ Body << "project"
+ << "/zooom";
+ Body << "engine"
+ << "/zooom";
+
+ zen::BinaryWriter MemOut;
+ IoBuffer BodyBuf = Body.Save().GetBuffer().AsIoBuffer();
+
+ auto Response = Http.Post("/prj/test"sv, BodyBuf);
+ CHECK(Response.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ auto Response = Http.Get("/prj/test"sv);
+ CHECK(Response.StatusCode == HttpResponseCode::OK);
+
+ CbObject ResponseObject = Response.AsObject();
+
+ CHECK(ResponseObject["id"].AsString() == "test"sv);
+ CHECK(ResponseObject["root"].AsString() == PathToUtf8(RootPath.c_str()));
+ }
+ }
+
+ BaseUri << "/prj/test/oplog/foobar";
+
+ {
+ HttpClient Http{BaseUri};
+
+ {
+ auto Response = Http.Post(""sv);
+ CHECK(Response.StatusCode == HttpResponseCode::Created);
+ }
+
+ {
+ auto Response = Http.Get(""sv);
+ CHECK(Response.StatusCode == HttpResponseCode::OK);
+
+ CbObject ResponseObject = Response.AsObject();
+
+ CHECK(ResponseObject["id"].AsString() == "foobar"sv);
+ CHECK(ResponseObject["project"].AsString() == "test"sv);
+ }
+ }
+
+ SUBCASE("build store persistence")
+ {
+ uint8_t AttachData[] = {1, 2, 3};
+
+ zen::CompressedBuffer Attachment = zen::CompressedBuffer::Compress(zen::SharedBuffer::Clone(zen::MemoryView{AttachData, 3}));
+ zen::CbAttachment Attach{Attachment, Attachment.DecodeRawHash()};
+
+ zen::CbObjectWriter OpWriter;
+ OpWriter << "key"
+ << "foo"
+ << "attachment" << Attach;
+
+ const std::string_view ChunkId{
+ "00000000"
+ "00000000"
+ "00010000"};
+ auto FileOid = zen::Oid::FromHexString(ChunkId);
+
+ OpWriter.BeginArray("files");
+ OpWriter.BeginObject();
+ OpWriter << "id" << FileOid;
+ OpWriter << "clientpath"
+ << "/{engine}/client/side/path";
+ OpWriter << "serverpath" << BinPath.c_str();
+ OpWriter.EndObject();
+ OpWriter.EndArray();
+
+ zen::CbObject Op = OpWriter.Save();
+
+ zen::CbPackage OpPackage(Op);
+ OpPackage.AddAttachment(Attach);
+
+ zen::BinaryWriter MemOut;
+ legacy::SaveCbPackage(OpPackage, MemOut);
+
+ HttpClient Http{BaseUri};
+
+ {
+ auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView()));
+
+ REQUIRE(Response);
+ CHECK(Response.StatusCode == HttpResponseCode::Created);
+ }
+
+ // Read file data
+
+ {
+ zen::StringBuilder<128> ChunkGetUri;
+ ChunkGetUri << "/" << ChunkId;
+ auto Response = Http.Get(ChunkGetUri);
+
+ REQUIRE(Response);
+ CHECK(Response.StatusCode == HttpResponseCode::OK);
+ }
+
+ {
+ zen::StringBuilder<128> ChunkGetUri;
+ ChunkGetUri << "/" << ChunkId << "?offset=1&size=10";
+ auto Response = Http.Get(ChunkGetUri);
+
+ REQUIRE(Response);
+ CHECK(Response.StatusCode == HttpResponseCode::OK);
+ CHECK(Response.ResponsePayload.GetSize() == 10);
+ }
+
+ ZEN_INFO("+++++++");
+ }
+
+ SUBCASE("snapshot")
+ {
+ zen::CbObjectWriter OpWriter;
+ OpWriter << "key"
+ << "foo";
+
+ const std::string_view ChunkId{
+ "00000000"
+ "00000000"
+ "00010000"};
+ auto FileOid = zen::Oid::FromHexString(ChunkId);
+
+ OpWriter.BeginArray("files");
+ OpWriter.BeginObject();
+ OpWriter << "id" << FileOid;
+ OpWriter << "clientpath"
+ << "/{engine}/client/side/path";
+ OpWriter << "serverpath" << BinPath.c_str();
+ OpWriter.EndObject();
+ OpWriter.EndArray();
+
+ zen::CbObject Op = OpWriter.Save();
+
+ zen::CbPackage OpPackage(Op);
+
+ zen::BinaryWriter MemOut;
+ legacy::SaveCbPackage(OpPackage, MemOut);
+
+ HttpClient Http{BaseUri};
+
+ {
+ auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView()));
+
+ REQUIRE(Response);
+ CHECK(Response.StatusCode == HttpResponseCode::Created);
+ }
+
+ // Read file data, it is raw and uncompressed
+ {
+ zen::StringBuilder<128> ChunkGetUri;
+ ChunkGetUri << "/" << ChunkId;
+ auto Response = Http.Get(ChunkGetUri);
+
+ REQUIRE(Response);
+ CHECK(Response.StatusCode == HttpResponseCode::OK);
+
+ IoBuffer Data = Response.ResponsePayload;
+ IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath);
+ CHECK(ReferenceData.GetSize() == Data.GetSize());
+ CHECK(ReferenceData.GetView().EqualBytes(Data.GetView()));
+ }
+
+ {
+ IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); });
+ auto Response = Http.Post("/rpc"sv, Payload, {{"Content-Type", "application/x-ue-cb"}});
+ REQUIRE(Response);
+ CHECK(Response.StatusCode == HttpResponseCode::OK);
+ }
+
+ // Read chunk data, it is now compressed
+ {
+ zen::StringBuilder<128> ChunkGetUri;
+ ChunkGetUri << "/" << ChunkId;
+ auto Response = Http.Get(ChunkGetUri, {{"Accept-Type", "application/x-ue-comp"}});
+
+ REQUIRE(Response);
+ CHECK(Response.StatusCode == HttpResponseCode::OK);
+
+ IoBuffer Data = Response.ResponsePayload;
+ IoHash RawHash;
+ uint64_t RawSize;
+ CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize);
+ CHECK(Compressed);
+ IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer();
+ IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath);
+ CHECK(RawSize == ReferenceData.GetSize());
+ CHECK(ReferenceData.GetSize() == DataDecompressed.GetSize());
+ CHECK(ReferenceData.GetView().EqualBytes(DataDecompressed.GetView()));
+ }
+
+ ZEN_INFO("+++++++");
+ }
+
+ SUBCASE("test chunk not found error")
+ {
+ HttpClient Http{BaseUri};
+
+ for (size_t I = 0; I < 65; I++)
+ {
+ zen::StringBuilder<128> PostUri;
+ PostUri << "/f77c781846caead318084604/info";
+ auto Response = Http.Get(PostUri);
+
+ REQUIRE(!Response.Error);
+ CHECK(Response.StatusCode == HttpResponseCode::NotFound);
+ }
+ }
+ }
+}
+
+CbPackage
+CreateOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments)
+{
+ CbPackage Package;
+ CbObjectWriter Object;
+ Object << "key"sv << OidAsString(Id);
+ if (!Attachments.empty())
+ {
+ Object.BeginArray("bulkdata");
+ for (const auto& Attachment : Attachments)
+ {
+ CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash());
+ Object.BeginObject();
+ Object << "id"sv << Attachment.first;
+ Object << "type"sv
+ << "Standard"sv;
+ Object << "data"sv << Attach;
+ Object.EndObject();
+
+ Package.AddAttachment(Attach);
+ ZEN_DEBUG("Added attachment {}", Attach.GetHash());
+ }
+ Object.EndArray();
+ }
+ Package.SetObject(Object.Save());
+ return Package;
+};
+
+CbObject
+CreateOplogOp(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments)
+{
+ CbObjectWriter Object;
+ Object << "key"sv << OidAsString(Id);
+ if (!Attachments.empty())
+ {
+ Object.BeginArray("bulkdata");
+ for (const auto& Attachment : Attachments)
+ {
+ CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash());
+ Object.BeginObject();
+ Object << "id"sv << Attachment.first;
+ Object << "type"sv
+ << "Standard"sv;
+ Object << "data"sv << Attach;
+ Object.EndObject();
+
+ ZEN_DEBUG("Added attachment {}", Attach.GetHash());
+ }
+ Object.EndArray();
+ }
+ return Object.Save();
+};
+
+enum CbWriterMeta
+{
+ BeginObject,
+ EndObject,
+ BeginArray,
+ EndArray
+};
+
+inline CbWriter&
+operator<<(CbWriter& Writer, CbWriterMeta Meta)
+{
+ switch (Meta)
+ {
+ case BeginObject:
+ Writer.BeginObject();
+ break;
+ case EndObject:
+ Writer.EndObject();
+ break;
+ case BeginArray:
+ Writer.BeginArray();
+ break;
+ case EndArray:
+ Writer.EndArray();
+ break;
+ default:
+ ZEN_ASSERT(false);
+ }
+ return Writer;
+}
+
+TEST_CASE("project.remote")
+{
+ using namespace std::literals;
+ using namespace utils;
+
+ ZenServerTestHelper Servers("remote", 3);
+ Servers.SpawnServers("--debug");
+
+ std::vector<Oid> OpIds;
+ const size_t OpCount = 24;
+ OpIds.reserve(OpCount);
+ for (size_t I = 0; I < OpCount; ++I)
+ {
+ OpIds.emplace_back(Oid::NewOid());
+ }
+
+ std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments;
+ {
+ std::vector<std::size_t> AttachmentSizes(
+ {7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 33466, 1093, 4269, 2257, 3685, 13489, 97194,
+ 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 224024, 51582, 5251, 491, 2u * 1024u * 1024u + 124u,
+ 74607, 18135, 3767, 154045, 4415, 5007, 8876, 96761, 3359, 8526, 4097, 4855, 48225});
+ auto It = AttachmentSizes.begin();
+ Attachments[OpIds[0]] = {};
+ Attachments[OpIds[1]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[2]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[3]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[4]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
+ Attachments[OpIds[5]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[6]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[7]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[8]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
+ Attachments[OpIds[9]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[10]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[11]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
+ Attachments[OpIds[12]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[13]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[14]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[15]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[16]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
+ Attachments[OpIds[17]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[18]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[19]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
+ Attachments[OpIds[20]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[21]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[22]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
+ Attachments[OpIds[23]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ ZEN_ASSERT(It == AttachmentSizes.end());
+ }
+
+ // Note: This is a clone of the function in projectstore.cpp
+ auto ComputeOpKey = [](const CbObjectView& Op) -> Oid {
+ using namespace std::literals;
+
+ XXH3_128Stream_deprecated KeyHasher;
+ Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); });
+ XXH3_128 KeyHash128 = KeyHasher.GetHash();
+
+ Oid KeyHash;
+ memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash);
+
+ return KeyHash;
+ };
+
+ auto AddOp = [ComputeOpKey](const CbObject& Op, std::unordered_map<Oid, uint32_t, Oid::Hasher>& Ops) {
+ const Oid Id = ComputeOpKey(Op);
+ IoBuffer Buffer = Op.GetBuffer().AsIoBuffer();
+ const uint32_t OpCoreHash = uint32_t(XXH3_64bits(Buffer.GetData(), Buffer.GetSize()) & 0xffffFFFF);
+ Ops.insert({Id, OpCoreHash});
+ };
+
+ auto MakeProject = [](std::string_view UrlBase, std::string_view ProjectName) {
+ CbObjectWriter Project;
+ Project.AddString("id"sv, ProjectName);
+ Project.AddString("root"sv, ""sv);
+ Project.AddString("engine"sv, ""sv);
+ Project.AddString("project"sv, ""sv);
+ Project.AddString("projectfile"sv, ""sv);
+ IoBuffer ProjectPayload = Project.Save().GetBuffer().AsIoBuffer();
+ ProjectPayload.SetContentType(HttpContentType::kCbObject);
+
+ HttpClient Http{UrlBase};
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}", ProjectName), ProjectPayload);
+ CHECK(Response);
+ };
+
+ auto MakeOplog = [](std::string_view UrlBase, std::string_view ProjectName, std::string_view OplogName) {
+ HttpClient Http{UrlBase};
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName), IoBuffer{});
+ CHECK(Response);
+ };
+
+ auto MakeOp = [](std::string_view UrlBase, std::string_view ProjectName, std::string_view OplogName, const CbPackage& OpPackage) {
+ zen::BinaryWriter MemOut;
+ legacy::SaveCbPackage(OpPackage, MemOut);
+ IoBuffer Body{IoBuffer::Wrap, MemOut.GetData(), MemOut.GetSize()};
+ Body.SetContentType(HttpContentType::kCbPackage);
+
+ HttpClient Http{UrlBase};
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/new", ProjectName, OplogName), Body);
+ CHECK(Response);
+ };
+
+ MakeProject(Servers.GetInstance(0).GetBaseUri(), "proj0");
+ MakeOplog(Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0");
+
+ std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps;
+ for (const Oid& OpId : OpIds)
+ {
+ CbPackage OpPackage = CreateOplogPackage(OpId, Attachments[OpId]);
+ CHECK(OpPackage.GetAttachments().size() == Attachments[OpId].size());
+ AddOp(OpPackage.GetObject(), SourceOps);
+ MakeOp(Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0", OpPackage);
+ }
+
+ std::vector<IoHash> AttachmentHashes;
+ AttachmentHashes.reserve(Attachments.size());
+ for (const auto& AttachmentOplog : Attachments)
+ {
+ for (const auto& Attachment : AttachmentOplog.second)
+ {
+ AttachmentHashes.emplace_back(Attachment.second.DecodeRawHash());
+ }
+ }
+
+ auto MakeCbObjectPayload = [](std::function<void(CbObjectWriter & Writer)> Write) -> IoBuffer {
+ CbObjectWriter Writer;
+ Write(Writer);
+ IoBuffer Result = Writer.Save().GetBuffer().AsIoBuffer();
+ Result.MakeOwned();
+ Result.SetContentType(HttpContentType::kCbObject);
+ return Result;
+ };
+
+ auto ValidateAttachments =
+ [&MakeCbObjectPayload, &AttachmentHashes, &Servers](int ServerIndex, std::string_view Project, std::string_view Oplog) {
+ HttpClient Http{Servers.GetInstance(ServerIndex).GetBaseUri()};
+
+ IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "getchunks"sv;
+ Writer << "chunks"sv << BeginArray;
+ for (const IoHash& Chunk : AttachmentHashes)
+ {
+ Writer << Chunk;
+ }
+ Writer << EndArray; // chunks
+ });
+
+ HttpClient::Response Response =
+ Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", Project, Oplog), Payload, {{"Accept", "application/x-ue-cbpkg"}});
+ CHECK(Response);
+ CbPackage ResponsePackage = ParsePackageMessage(Response.ResponsePayload);
+ CHECK(ResponsePackage.GetAttachments().size() == AttachmentHashes.size());
+ for (auto A : ResponsePackage.GetAttachments())
+ {
+ CHECK(IoHash::HashBuffer(A.AsCompressedBinary().DecompressToComposite()) == A.GetHash());
+ }
+ };
+
+ auto ValidateOplog = [&SourceOps, &AddOp, &Servers](int ServerIndex, std::string_view Project, std::string_view Oplog) {
+ std::unordered_map<Oid, uint32_t, Oid::Hasher> TargetOps;
+ std::vector<CbObject> ResultingOplog;
+
+ HttpClient Http{Servers.GetInstance(ServerIndex).GetBaseUri()};
+ HttpClient::Response Response = Http.Get(fmt::format("/prj/{}/oplog/{}/entries", Project, Oplog));
+ CHECK(Response);
+
+ IoBuffer Payload(Response.ResponsePayload);
+ CbObject OplogResonse = LoadCompactBinaryObject(Payload);
+ CbArrayView EntriesArray = OplogResonse["entries"sv].AsArrayView();
+
+ for (CbFieldView OpEntry : EntriesArray)
+ {
+ CbObjectView Core = OpEntry.AsObjectView();
+ BinaryWriter Writer;
+ Core.CopyTo(Writer);
+ MemoryView OpView = Writer.GetView();
+ IoBuffer OpBuffer(IoBuffer::Wrap, OpView.GetData(), OpView.GetSize());
+ CbObject Op(SharedBuffer(OpBuffer), CbFieldType::HasFieldType);
+ AddOp(Op, TargetOps);
+ }
+ CHECK(SourceOps == TargetOps);
+ };
+
+ auto HttpWaitForCompletion = [](ZenServerInstance& Server, const HttpClient::Response& Response) {
+ REQUIRE(Response);
+ const uint64_t JobId = ParseInt<uint64_t>(Response.AsText()).value_or(0);
+ CHECK(JobId != 0);
+
+ HttpClient Http{Server.GetBaseUri()};
+
+ while (true)
+ {
+ HttpClient::Response StatusResponse =
+ Http.Get(fmt::format("/admin/jobs/{}", JobId), {{"Accept", ToString(ZenContentType::kCbObject)}});
+ CHECK(StatusResponse);
+ CbObject ResponseObject = StatusResponse.AsObject();
+ std::string_view Status = ResponseObject["Status"sv].AsString();
+ CHECK(Status != "Aborted"sv);
+ if (Status == "Complete"sv)
+ {
+ return;
+ }
+ Sleep(10);
+ }
+ };
+
+ SUBCASE("File")
+ {
+ ScopedTemporaryDirectory TempDir;
+ {
+ IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "export"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "maxblocksize"sv << 3072u;
+ Writer << "maxchunkembedsize"sv << 1296u;
+ Writer << "chunkfilesizelimit"sv << 5u * 1024u;
+ Writer << "force"sv << false;
+ Writer << "file"sv << BeginObject;
+ {
+ Writer << "path"sv << path;
+ Writer << "name"sv
+ << "proj0_oplog0"sv;
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
+
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(0), Response);
+ }
+ {
+ MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy");
+ MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy");
+
+ IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "import"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "force"sv << false;
+ Writer << "file"sv << BeginObject;
+ {
+ Writer << "path"sv << path;
+ Writer << "name"sv
+ << "proj0_oplog0"sv;
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(1).GetBaseUri()};
+
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(1), Response);
+ }
+ ValidateAttachments(1, "proj0_copy", "oplog0_copy");
+ ValidateOplog(1, "proj0_copy", "oplog0_copy");
+ }
+
+ SUBCASE("File disable blocks")
+ {
+ ScopedTemporaryDirectory TempDir;
+ {
+ IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "export"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "maxblocksize"sv << 3072u;
+ Writer << "maxchunkembedsize"sv << 1296u;
+ Writer << "chunkfilesizelimit"sv << 5u * 1024u;
+ Writer << "force"sv << false;
+ Writer << "file"sv << BeginObject;
+ {
+ Writer << "path"sv << TempDir.Path().string();
+ Writer << "name"sv
+ << "proj0_oplog0"sv;
+ Writer << "disableblocks"sv << true;
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
+
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(0), Response);
+ }
+ {
+ MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy");
+ MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy");
+ IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "import"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "force"sv << false;
+ Writer << "file"sv << BeginObject;
+ {
+ Writer << "path"sv << TempDir.Path().string();
+ Writer << "name"sv
+ << "proj0_oplog0"sv;
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(1).GetBaseUri()};
+
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(1), Response);
+ }
+ ValidateAttachments(1, "proj0_copy", "oplog0_copy");
+ ValidateOplog(1, "proj0_copy", "oplog0_copy");
+ }
+
+ SUBCASE("File force temp blocks")
+ {
+ ScopedTemporaryDirectory TempDir;
+ {
+ IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "export"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "maxblocksize"sv << 3072u;
+ Writer << "maxchunkembedsize"sv << 1296u;
+ Writer << "chunkfilesizelimit"sv << 5u * 1024u;
+ Writer << "force"sv << false;
+ Writer << "file"sv << BeginObject;
+ {
+ Writer << "path"sv << TempDir.Path().string();
+ Writer << "name"sv
+ << "proj0_oplog0"sv;
+ Writer << "enabletempblocks"sv << true;
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(0), Response);
+ }
+ {
+ MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy");
+ MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy");
+ IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "import"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "force"sv << false;
+ Writer << "file"sv << BeginObject;
+ {
+ Writer << "path"sv << TempDir.Path().string();
+ Writer << "name"sv
+ << "proj0_oplog0"sv;
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(1).GetBaseUri()};
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(1), Response);
+ }
+ ValidateAttachments(1, "proj0_copy", "oplog0_copy");
+ ValidateOplog(1, "proj0_copy", "oplog0_copy");
+ }
+
+ SUBCASE("Zen")
+ {
+ ScopedTemporaryDirectory TempDir;
+ {
+ std::string ExportSourceUri = Servers.GetInstance(0).GetBaseUri();
+ std::string ExportTargetUri = Servers.GetInstance(1).GetBaseUri();
+ MakeProject(ExportTargetUri, "proj0_copy");
+ MakeOplog(ExportTargetUri, "proj0_copy", "oplog0_copy");
+
+ IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "export"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "maxblocksize"sv << 3072u;
+ Writer << "maxchunkembedsize"sv << 1296u;
+ Writer << "chunkfilesizelimit"sv << 5u * 1024u;
+ Writer << "force"sv << false;
+ Writer << "zen"sv << BeginObject;
+ {
+ Writer << "url"sv << ExportTargetUri.substr(7);
+ Writer << "project"
+ << "proj0_copy";
+ Writer << "oplog"
+ << "oplog0_copy";
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(0), Response);
+ }
+ ValidateAttachments(1, "proj0_copy", "oplog0_copy");
+ ValidateOplog(1, "proj0_copy", "oplog0_copy");
+
+ {
+ std::string ImportSourceUri = Servers.GetInstance(1).GetBaseUri();
+ std::string ImportTargetUri = Servers.GetInstance(2).GetBaseUri();
+ MakeProject(ImportTargetUri, "proj1");
+ MakeOplog(ImportTargetUri, "proj1", "oplog1");
+
+ IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
+ Writer << "method"sv
+ << "import"sv;
+ Writer << "params" << BeginObject;
+ {
+ Writer << "force"sv << false;
+ Writer << "zen"sv << BeginObject;
+ {
+ Writer << "url"sv << ImportSourceUri.substr(7);
+ Writer << "project"
+ << "proj0_copy";
+ Writer << "oplog"
+ << "oplog0_copy";
+ }
+ Writer << EndObject; // "file"
+ }
+ Writer << EndObject; // "params"
+ });
+
+ HttpClient Http{Servers.GetInstance(2).GetBaseUri()};
+ HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj1", "oplog1"), Payload);
+ HttpWaitForCompletion(Servers.GetInstance(2), Response);
+ }
+ ValidateAttachments(2, "proj1", "oplog1");
+ ValidateOplog(2, "proj1", "oplog1");
+ }
+}
+
+TEST_CASE("project.rpcappendop")
+{
+ using namespace std::literals;
+ using namespace utils;
+
+ ZenServerTestHelper Servers("remote", 2);
+ Servers.SpawnServers("--debug");
+
+ std::vector<Oid> OpIds;
+ const size_t OpCount = 24;
+ OpIds.reserve(OpCount);
+ for (size_t I = 0; I < OpCount; ++I)
+ {
+ OpIds.emplace_back(Oid::NewOid());
+ }
+
+ std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments;
+ {
+ std::vector<std::size_t> AttachmentSizes(
+ {7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 33466, 1093, 4269, 2257, 3685, 13489, 97194,
+ 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 224024, 51582, 5251, 491, 2u * 1024u * 1024u + 124u,
+ 74607, 18135, 3767, 154045, 4415, 5007, 8876, 96761, 3359, 8526, 4097, 4855, 48225});
+ auto It = AttachmentSizes.begin();
+ Attachments[OpIds[0]] = {};
+ Attachments[OpIds[1]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[2]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[3]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[4]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
+ Attachments[OpIds[5]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[6]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[7]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[8]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
+ Attachments[OpIds[9]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[10]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[11]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
+ Attachments[OpIds[12]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
+ Attachments[OpIds[13]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[14]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[15]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[16]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
+ Attachments[OpIds[17]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[18]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
+ Attachments[OpIds[19]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
+ Attachments[OpIds[20]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[21]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ Attachments[OpIds[22]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
+ Attachments[OpIds[23]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
+ ZEN_ASSERT(It == AttachmentSizes.end());
+ }
+
+ // Note: This is a clone of the function in projectstore.cpp
+ auto ComputeOpKey = [](const CbObjectView& Op) -> Oid {
+ using namespace std::literals;
+
+ XXH3_128Stream_deprecated KeyHasher;
+ Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); });
+ XXH3_128 KeyHash128 = KeyHasher.GetHash();
+
+ Oid KeyHash;
+ memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash);
+
+ return KeyHash;
+ };
+
+ auto AddOp = [ComputeOpKey](const CbObject& Op, std::unordered_map<Oid, uint32_t, Oid::Hasher>& Ops) {
+ const Oid Id = ComputeOpKey(Op);
+ IoBuffer Buffer = Op.GetBuffer().AsIoBuffer();
+ const uint32_t OpCoreHash = uint32_t(XXH3_64bits(Buffer.GetData(), Buffer.GetSize()) & 0xffffFFFF);
+ Ops.insert({Id, OpCoreHash});
+ };
+
+ auto MakeProject = [](HttpClient& Client, std::string_view ProjectName) {
+ CbObjectWriter Project;
+ Project.AddString("id"sv, ProjectName);
+ Project.AddString("root"sv, ""sv);
+ Project.AddString("engine"sv, ""sv);
+ Project.AddString("project"sv, ""sv);
+ Project.AddString("projectfile"sv, ""sv);
+ HttpClient::Response Response = Client.Post(fmt::format("/prj/{}", ProjectName), Project.Save());
+ CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
+ };
+
+ auto MakeOplog = [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName) {
+ HttpClient::Response Response = Client.Post(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName));
+ CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
+ };
+ auto GetOplog = [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName) {
+ HttpClient::Response Response = Client.Get(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName));
+ CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
+ return Response.AsObject();
+ };
+
+ auto MakeOp =
+ [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName, const CbObjectView& Op) -> std::vector<IoHash> {
+ CbObjectWriter Request;
+ Request.AddString("method"sv, "appendops"sv);
+ Request.BeginArray("ops"sv);
+ {
+ Request.AddObject(Op);
+ }
+ Request.EndArray(); // "ops"
+ HttpClient::Response Response = Client.Post(fmt::format("/prj/{}/oplog/{}/rpc", ProjectName, OplogName), Request.Save());
+ CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
+
+ CbObjectView ResponsePayload = Response.AsPackage().GetObject();
+ CbArrayView NeedArray = ResponsePayload["need"sv].AsArrayView();
+ std::vector<IoHash> Needs;
+ Needs.reserve(NeedArray.Num());
+ for (CbFieldView NeedView : NeedArray)
+ {
+ Needs.push_back(NeedView.AsHash());
+ }
+ return Needs;
+ };
+
+ auto SendAttachments = [](HttpClient& Client,
+ std::string_view ProjectName,
+ std::string_view OplogName,
+ std::span<const CompressedBuffer> Attachments,
+ void* ServerProcessHandle,
+ const std::filesystem::path& TempPath) {
+ CompositeBuffer PackageMessage;
+ {
+ CbPackage RequestPackage;
+ CbObjectWriter Request;
+ Request.AddString("method"sv, "putchunks"sv);
+ Request.AddBool("usingtmpfiles"sv, true);
+ Request.BeginArray("chunks"sv);
+ for (CompressedBuffer AttachmentPayload : Attachments)
+ {
+ if (AttachmentPayload.DecodeRawSize() > 16u * 1024u)
+ {
+ std::filesystem::path TempAttachmentPath = TempPath / (Oid::NewOid().ToString() + ".tmp");
+ WriteFile(TempAttachmentPath, AttachmentPayload.GetCompressed());
+ IoBuffer OnDiskAttachment = IoBufferBuilder::MakeFromFile(TempAttachmentPath);
+ AttachmentPayload = CompressedBuffer::FromCompressedNoValidate(std::move(OnDiskAttachment));
+ }
+
+ CbAttachment Attachment(AttachmentPayload, AttachmentPayload.DecodeRawHash());
+
+ Request.AddAttachment(Attachment);
+ RequestPackage.AddAttachment(Attachment);
+ }
+ Request.EndArray(); // "chunks"
+ RequestPackage.SetObject(Request.Save());
+
+ PackageMessage = CompositeBuffer(FormatPackageMessage(RequestPackage, FormatFlags::kAllowLocalReferences, ServerProcessHandle));
+ }
+
+ HttpClient::Response Response =
+ Client.Post(fmt::format("/prj/{}/oplog/{}/rpc", ProjectName, OplogName), PackageMessage, HttpContentType::kCbPackage);
+ CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
+ };
+
+ {
+ HttpClient Client(Servers.GetInstance(0).GetBaseUri());
+ void* ServerProcessHandle = Servers.GetInstance(0).GetProcessHandle();
+
+ MakeProject(Client, "proj0");
+ MakeOplog(Client, "proj0", "oplog0");
+ CbObject Oplog = GetOplog(Client, "proj0", "oplog0");
+ std::filesystem::path TempPath = Oplog["tempdir"sv].AsU8String();
+
+ std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps;
+ for (const Oid& OpId : OpIds)
+ {
+ CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
+ AddOp(Op, SourceOps);
+ std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
+
+ if (!MissingAttachments.empty())
+ {
+ CHECK(MissingAttachments.size() <= Attachments[OpId].size());
+ tsl::robin_set<IoHash, IoHash::Hasher> MissingAttachmentSet(MissingAttachments.begin(), MissingAttachments.end());
+ std::vector<CompressedBuffer> PutAttachments;
+ for (const auto& Attachment : Attachments[OpId])
+ {
+ CompressedBuffer Payload = Attachment.second;
+ const IoHash AttachmentHash = Payload.DecodeRawHash();
+ if (auto It = MissingAttachmentSet.find(AttachmentHash); It != MissingAttachmentSet.end())
+ {
+ PutAttachments.push_back(Payload);
+ }
+ }
+ SendAttachments(Client, "proj0", "oplog0", PutAttachments, ServerProcessHandle, TempPath);
+ }
+ }
+
+ // Do it again, but now we should not need any attachments
+
+ for (const Oid& OpId : OpIds)
+ {
+ CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
+ AddOp(Op, SourceOps);
+ std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
+ CHECK(MissingAttachments.empty());
+ }
+ }
+
+ {
+ HttpClient Client(Servers.GetInstance(1).GetBaseUri());
+ void* ServerProcessHandle = nullptr; // Force use of path for attachments passed on disk
+
+ MakeProject(Client, "proj0");
+ MakeOplog(Client, "proj0", "oplog0");
+ CbObject Oplog = GetOplog(Client, "proj0", "oplog0");
+ std::filesystem::path TempPath = Oplog["tempdir"sv].AsU8String();
+
+ std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps;
+ for (const Oid& OpId : OpIds)
+ {
+ CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
+ AddOp(Op, SourceOps);
+ std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
+
+ if (!MissingAttachments.empty())
+ {
+ CHECK(MissingAttachments.size() <= Attachments[OpId].size());
+ tsl::robin_set<IoHash, IoHash::Hasher> MissingAttachmentSet(MissingAttachments.begin(), MissingAttachments.end());
+ std::vector<CompressedBuffer> PutAttachments;
+ for (const auto& Attachment : Attachments[OpId])
+ {
+ CompressedBuffer Payload = Attachment.second;
+ const IoHash AttachmentHash = Payload.DecodeRawHash();
+ if (auto It = MissingAttachmentSet.find(AttachmentHash); It != MissingAttachmentSet.end())
+ {
+ PutAttachments.push_back(Payload);
+ }
+ }
+ SendAttachments(Client, "proj0", "oplog0", PutAttachments, ServerProcessHandle, TempPath);
+ }
+ }
+
+ // Do it again, but now we should not need any attachments
+
+ for (const Oid& OpId : OpIds)
+ {
+ CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
+ AddOp(Op, SourceOps);
+ std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
+ CHECK(MissingAttachments.empty());
+ }
+ }
+}
+
+} // namespace zen::tests
+
+#endif
diff --git a/src/zenserver-test/workspace-tests.cpp b/src/zenserver-test/workspace-tests.cpp
new file mode 100644
index 000000000..f299b6dcf
--- /dev/null
+++ b/src/zenserver-test/workspace-tests.cpp
@@ -0,0 +1,541 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#if ZEN_WITH_TESTS
+# include "zenserver-test.h"
+# include <zencore/testing.h>
+# include <zencore/testutils.h>
+# include <zencore/workthreadpool.h>
+# include <zencore/compactbinarybuilder.h>
+# include <zencore/compactbinarypackage.h>
+# include <zencore/compress.h>
+# include <zencore/fmtutils.h>
+# include <zencore/filesystem.h>
+# include <zencore/stream.h>
+# include <zencore/string.h>
+# include <zenutil/zenserverprocess.h>
+# include <zenutil/chunkrequests.h>
+# include <zenhttp/httpclient.h>
+
+namespace zen::tests {
+
+using namespace std::literals;
+
+std::vector<std::pair<std::filesystem::path, IoBuffer>>
+GenerateFolderContent(const std::filesystem::path& RootPath)
+{
+ CreateDirectories(RootPath);
+ std::vector<std::pair<std::filesystem::path, IoBuffer>> Result;
+ Result.push_back(std::make_pair(RootPath / "root_blob_1.bin", CreateRandomBlob(4122)));
+ Result.push_back(std::make_pair(RootPath / "root_blob_2.bin", CreateRandomBlob(2122)));
+
+ std::filesystem::path EmptyFolder(RootPath / "empty_folder");
+
+ std::filesystem::path FirstFolder(RootPath / "first_folder");
+ CreateDirectories(FirstFolder);
+ Result.push_back(std::make_pair(FirstFolder / "first_folder_blob1.bin", CreateRandomBlob(22)));
+ Result.push_back(std::make_pair(FirstFolder / "first_folder_blob2.bin", CreateRandomBlob(122)));
+
+ std::filesystem::path SecondFolder(RootPath / "second_folder");
+ CreateDirectories(SecondFolder);
+ Result.push_back(std::make_pair(SecondFolder / "second_folder_blob1.bin", CreateRandomBlob(522)));
+ Result.push_back(std::make_pair(SecondFolder / "second_folder_blob2.bin", CreateRandomBlob(122)));
+ Result.push_back(std::make_pair(SecondFolder / "second_folder_blob3.bin", CreateRandomBlob(225)));
+
+ std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second");
+ CreateDirectories(SecondFolderChild);
+ Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob1.bin", CreateRandomBlob(622)));
+
+ for (const auto& It : Result)
+ {
+ WriteFile(It.first, It.second);
+ }
+
+ return Result;
+}
+
+std::vector<std::pair<std::filesystem::path, IoBuffer>>
+GenerateFolderContent2(const std::filesystem::path& RootPath)
+{
+ std::vector<std::pair<std::filesystem::path, IoBuffer>> Result;
+ Result.push_back(std::make_pair(RootPath / "root_blob_3.bin", CreateRandomBlob(312)));
+ std::filesystem::path FirstFolder(RootPath / "first_folder");
+ Result.push_back(std::make_pair(FirstFolder / "first_folder_blob3.bin", CreateRandomBlob(722)));
+ std::filesystem::path SecondFolder(RootPath / "second_folder");
+ std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second");
+ Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob2.bin", CreateRandomBlob(962)));
+ Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob3.bin", CreateRandomBlob(561)));
+
+ for (const auto& It : Result)
+ {
+ WriteFile(It.first, It.second);
+ }
+
+ return Result;
+}
+
+TEST_CASE("workspaces.create")
+{
+ using namespace std::literals;
+
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
+ fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path Root1Path = TempDir.Path() / "root1";
+ std::filesystem::path Root2Path = TempDir.Path() / "root2";
+ DeleteDirectories(Root1Path);
+ DeleteDirectories(Root2Path);
+
+ std::filesystem::path Share1Path = "shared_1";
+ std::filesystem::path Share2Path = "shared_2";
+ CreateDirectories(Root1Path / Share1Path);
+ CreateDirectories(Root1Path / Share2Path);
+ CreateDirectories(Root2Path / Share1Path);
+ CreateDirectories(Root2Path / Share2Path);
+
+ Oid Root1Id = Oid::Zero;
+ Oid Root2Id = Oid::NewOid();
+
+ HttpClient Client(Instance.GetBaseUri());
+
+ CHECK(Client.Put(fmt::format("/ws/{}", Root1Id)).StatusCode == HttpResponseCode::BadRequest);
+
+ if (HttpClient::Response Root1Response =
+ Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
+ Root1Response.StatusCode == HttpResponseCode::Created)
+ {
+ Root1Id = Oid::TryFromHexString(Root1Response.AsText());
+ CHECK(Root1Id != Oid::Zero);
+ }
+ else
+ {
+ CHECK(false);
+ }
+ if (HttpClient::Response Root1Response =
+ Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
+ Root1Response.StatusCode == HttpResponseCode::OK)
+ {
+ CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText()));
+ }
+ else
+ {
+ CHECK(false);
+ }
+ if (HttpClient::Response Root1Response =
+ Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
+ Root1Response.StatusCode == HttpResponseCode::OK)
+ {
+ CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText()));
+ }
+ else
+ {
+ CHECK(false);
+ }
+ CHECK(Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}).StatusCode ==
+ HttpResponseCode::Conflict);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::NotFound);
+
+ CHECK(Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode ==
+ HttpResponseCode::Conflict);
+
+ if (HttpClient::Response Root2Response =
+ Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}});
+ Root2Response.StatusCode == HttpResponseCode::Created)
+ {
+ CHECK(Root2Id == Oid::TryFromHexString(Root2Response.AsText()));
+ }
+ else
+ {
+ CHECK(false);
+ }
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero)).StatusCode == HttpResponseCode::BadRequest);
+
+ Oid Share2Id = Oid::Zero;
+ if (HttpClient::Response Share2Response =
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}});
+ Share2Response.StatusCode == HttpResponseCode::Created)
+ {
+ Share2Id = Oid::TryFromHexString(Share2Response.AsText());
+ CHECK(Share2Id != Oid::Zero);
+ }
+ else
+ {
+ CHECK(false);
+ }
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::OK);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::OK);
+
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode ==
+ HttpResponseCode::Conflict);
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::NewOid()), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}})
+ .StatusCode == HttpResponseCode::Conflict);
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", "idonotexist"}}).StatusCode !=
+ HttpResponseCode::OK);
+
+ while (true)
+ {
+ std::error_code Ec;
+ DeleteDirectories(Root2Path / Share2Path, Ec);
+ if (!Ec)
+ break;
+ }
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/files", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound);
+}
+
+TEST_CASE("workspaces.restricted")
+{
+ using namespace std::literals;
+
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path Root1Path = TempDir.Path() / "root1";
+ std::filesystem::path Root2Path = TempDir.Path() / "root2";
+ DeleteDirectories(Root1Path);
+ DeleteDirectories(Root2Path);
+
+ std::filesystem::path Share1Path = "shared_1";
+ std::filesystem::path Share2Path = "shared_2";
+ CreateDirectories(Root1Path / Share1Path);
+ CreateDirectories(Root1Path / Share2Path);
+ CreateDirectories(Root2Path / Share1Path);
+ CreateDirectories(Root2Path / Share2Path);
+
+ Oid Root1Id = Oid::NewOid();
+ Oid Root2Id = Oid::NewOid();
+ Oid Share1Id = Oid::NewOid();
+ Oid Share2Id = Oid::NewOid();
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode ==
+ HttpResponseCode::Unauthorized);
+
+ CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::NotFound);
+
+ std::string Config1;
+ {
+ CbObjectWriter Config;
+ Config.BeginArray("workspaces");
+ Config.BeginObject();
+ Config << "id"sv << Root1Id.ToString();
+ Config << "root_path"sv << Root1Path.string();
+ Config << "allow_share_creation_from_http"sv << false;
+ Config.EndObject();
+ Config.EndArray();
+ ExtendableStringBuilder<256> SB;
+ CompactBinaryToJson(Config.Save(), SB);
+ Config1 = SB.ToString();
+ }
+ WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config1.data(), Config1.size()));
+
+ CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode));
+
+ CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::OK);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", Root1Id, Share1Id)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode ==
+ HttpResponseCode::Unauthorized);
+
+ std::string Config2;
+ {
+ CbObjectWriter Config;
+ Config.BeginArray("workspaces");
+ Config.BeginObject();
+ Config << "id"sv << Root1Id.ToString();
+ Config << "root_path"sv << Root1Path.string();
+ Config << "allow_share_creation_from_http"sv << false;
+ Config.EndObject();
+ Config.BeginObject();
+ Config << "id"sv << Root2Id.ToString();
+ Config << "root_path"sv << Root2Path.string();
+ Config << "allow_share_creation_from_http"sv << true;
+ Config.EndObject();
+ Config.EndArray();
+ ExtendableStringBuilder<256> SB;
+ CompactBinaryToJson(Config.Save(), SB);
+ Config2 = SB.ToString();
+ }
+ WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config2.data(), Config2.size()));
+
+ CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode));
+
+ CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root2Id)).StatusCode, HttpResponseCode::OK);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(
+ Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::OK);
+
+ CHECK(IsHttpSuccessCode(Client.Delete(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode));
+}
+
+TEST_CASE("workspaces.lifetimes")
+{
+ using namespace std::literals;
+
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ Oid WorkspaceId = Oid::NewOid();
+ Oid ShareId = Oid::NewOid();
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path RootPath = TempDir.Path();
+ DeleteDirectories(RootPath);
+ std::filesystem::path SharePath = RootPath / "shared_folder";
+ CreateDirectories(SharePath);
+
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
+ fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
+ CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
+ HttpResponseCode::OK);
+
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}})
+ .StatusCode == HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}})
+ .StatusCode == HttpResponseCode::OK);
+ }
+
+ // Restart
+
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
+ }
+
+ // Wipe system config
+ DeleteDirectories(SystemRootPath);
+
+ // Restart
+
+ {
+ std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
+ ZenServerInstance Instance(TestEnv);
+ Instance.SetTestDir(TestDir);
+ const uint16_t PortNumber =
+ Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ HttpClient Client(Instance.GetBaseUri());
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound);
+ }
+}
+
+TEST_CASE("workspaces.share")
+{
+ std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
+
+ ZenServerInstance Instance(TestEnv);
+
+ const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
+ fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
+ CHECK(PortNumber != 0);
+
+ ScopedTemporaryDirectory TempDir;
+ std::filesystem::path RootPath = TempDir.Path();
+ DeleteDirectories(RootPath);
+ std::filesystem::path SharePath = RootPath / "shared_folder";
+ GenerateFolderContent(SharePath);
+
+ HttpClient Client(Instance.GetBaseUri());
+
+ Oid WorkspaceId = Oid::NewOid();
+ CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
+
+ Oid ShareId = Oid::NewOid();
+ CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}).StatusCode ==
+ HttpResponseCode::Created);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8);
+ GenerateFolderContent2(SharePath);
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8);
+ HttpClient::Response FilesResponse =
+ Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId),
+ {},
+ HttpClient::KeyValueMap{{"refresh", ToString(true)}, {"fieldnames", "id,clientpath,size"}});
+ CHECK(FilesResponse);
+ std::unordered_map<Oid, std::pair<std::filesystem::path, uint64_t>, Oid::Hasher> Files;
+ {
+ CbArrayView FilesArray = FilesResponse.AsObject()["files"sv].AsArrayView();
+ CHECK(FilesArray.Num() == 12);
+ for (CbFieldView Field : FilesArray)
+ {
+ CbObjectView FileObject = Field.AsObjectView();
+ Oid ChunkId = FileObject["id"sv].AsObjectId();
+ CHECK(ChunkId != Oid::Zero);
+ uint64_t Size = FileObject["size"sv].AsUInt64();
+ std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
+ std::filesystem::path AbsFilePath = SharePath / Path;
+ CHECK(IsFile(AbsFilePath));
+ CHECK(FileSizeFromPath(AbsFilePath) == Size);
+ Files.insert_or_assign(ChunkId, std::make_pair(AbsFilePath, Size));
+ }
+ }
+
+ HttpClient::Response EntriesResponse =
+ Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId), {}, HttpClient::KeyValueMap{{"fieldfilter", "id,clientpath"}});
+ CHECK(EntriesResponse);
+ {
+ CbArrayView EntriesArray = EntriesResponse.AsObject()["entries"sv].AsArrayView();
+ CHECK(EntriesArray.Num() == 1);
+ for (CbFieldView EntryField : EntriesArray)
+ {
+ CbObjectView EntryObject = EntryField.AsObjectView();
+ CbArrayView FilesArray = EntryObject["files"sv].AsArrayView();
+ CHECK(FilesArray.Num() == 12);
+ for (CbFieldView FileField : FilesArray)
+ {
+ CbObjectView FileObject = FileField.AsObjectView();
+ Oid ChunkId = FileObject["id"sv].AsObjectId();
+ CHECK(ChunkId != Oid::Zero);
+ std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
+ std::filesystem::path AbsFilePath = SharePath / Path;
+ CHECK(IsFile(AbsFilePath));
+ }
+ }
+ }
+
+ HttpClient::Response FileManifestResponse =
+ Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId),
+ {},
+ HttpClient::KeyValueMap{{"opkey", "file_manifest"}, {"fieldfilter", "id,clientpath"}});
+ CHECK(FileManifestResponse);
+ {
+ CbArrayView EntriesArray = FileManifestResponse.AsObject()["entry"sv].AsObjectView()["files"sv].AsArrayView();
+ CHECK(EntriesArray.Num() == 12);
+ for (CbFieldView Field : EntriesArray)
+ {
+ CbObjectView FileObject = Field.AsObjectView();
+ Oid ChunkId = FileObject["id"sv].AsObjectId();
+ CHECK(ChunkId != Oid::Zero);
+ std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
+ std::filesystem::path AbsFilePath = SharePath / Path;
+ CHECK(IsFile(AbsFilePath));
+ }
+ }
+
+ for (auto It : Files)
+ {
+ const Oid& ChunkId = It.first;
+ const std::filesystem::path& Path = It.second.first;
+ const uint64_t Size = It.second.second;
+
+ CHECK(Client.Get(fmt::format("/ws/{}/{}/{}/info", WorkspaceId, ShareId, ChunkId)).AsObject()["size"sv].AsUInt64() == Size);
+
+ {
+ IoBuffer Payload = Client.Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId)).ResponsePayload;
+ CHECK(Payload);
+ CHECK(Payload.GetSize() == Size);
+ IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path);
+ CHECK(FileContent);
+ CHECK(FileContent.GetView().EqualBytes(Payload.GetView()));
+ }
+
+ {
+ IoBuffer Payload =
+ Client
+ .Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId),
+ {},
+ HttpClient::KeyValueMap{{"offset", fmt::format("{}", Size / 4)}, {"size", fmt::format("{}", Size / 2)}})
+ .ResponsePayload;
+ CHECK(Payload);
+ CHECK(Payload.GetSize() == Size / 2);
+ IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Size / 4, Size / 2);
+ CHECK(FileContent);
+ CHECK(FileContent.GetView().EqualBytes(Payload.GetView()));
+ }
+ }
+
+ {
+ uint32_t CorrelationId = gsl::narrow<uint32_t>(Files.size());
+ std::vector<RequestChunkEntry> BatchEntries;
+ for (auto It : Files)
+ {
+ const Oid& ChunkId = It.first;
+ const uint64_t Size = It.second.second;
+
+ BatchEntries.push_back(
+ RequestChunkEntry{.ChunkId = ChunkId, .CorrelationId = --CorrelationId, .Offset = Size / 4, .RequestBytes = Size / 2});
+ }
+ IoBuffer BatchResponse =
+ Client.Post(fmt::format("/ws/{}/{}/batch", WorkspaceId, ShareId), BuildChunkBatchRequest(BatchEntries)).ResponsePayload;
+ CHECK(BatchResponse);
+ std::vector<IoBuffer> BatchResult = ParseChunkBatchResponse(BatchResponse);
+ CHECK(BatchResult.size() == Files.size());
+ for (const RequestChunkEntry& Request : BatchEntries)
+ {
+ IoBuffer Result = BatchResult[Request.CorrelationId];
+ auto It = Files.find(Request.ChunkId);
+ const std::filesystem::path& Path = It->second.first;
+ CHECK(Result.GetSize() == Request.RequestBytes);
+ IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Request.Offset, Request.RequestBytes);
+ CHECK(FileContent);
+ CHECK(FileContent.GetView().EqualBytes(Result.GetView()));
+ }
+ }
+
+ CHECK(Client.Delete(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)));
+ CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound);
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)));
+
+ CHECK(Client.Delete(fmt::format("/ws/{}", WorkspaceId)));
+ CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound);
+}
+
+} // namespace zen::tests
+#endif
diff --git a/src/zenserver-test/zenserver-test.cpp b/src/zenserver-test/zenserver-test.cpp
index 827a4eb5a..773383954 100644
--- a/src/zenserver-test/zenserver-test.cpp
+++ b/src/zenserver-test/zenserver-test.cpp
@@ -2,73 +2,32 @@
#define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING
-#include <zenbase/refcount.h>
-#include <zencore/compactbinary.h>
-#include <zencore/compactbinarybuilder.h>
-#include <zencore/compactbinarypackage.h>
-#include <zencore/compress.h>
-#include <zencore/except.h>
-#include <zencore/filesystem.h>
-#include <zencore/fmtutils.h>
-#include <zencore/iohash.h>
-#include <zencore/logging.h>
-#include <zencore/memoryview.h>
-#include <zencore/scopeguard.h>
-#include <zencore/stream.h>
-#include <zencore/string.h>
-#include <zencore/testutils.h>
-#include <zencore/thread.h>
-#include <zencore/timer.h>
-#include <zencore/xxhash.h>
-#include <zenhttp/httpclient.h>
-#include <zenhttp/packageformat.h>
-#include <zenhttp/zenhttp.h>
-#include <zenutil/buildstoragecache.h>
-#include <zenutil/cache/cache.h>
-#include <zenutil/cache/cacherequests.h>
-#include <zenutil/chunkrequests.h>
-#include <zenutil/logging/testformatter.h>
-#include <zenutil/zenserverprocess.h>
-
-#include <http_parser.h>
-
-#if ZEN_PLATFORM_WINDOWS
-# pragma comment(lib, "Crypt32.lib")
-# pragma comment(lib, "Wldap32.lib")
-#endif
-
-ZEN_THIRD_PARTY_INCLUDES_START
-#include <tsl/robin_set.h>
-#undef GetObject
-ZEN_THIRD_PARTY_INCLUDES_END
-
-#include <atomic>
-#include <filesystem>
-#include <map>
-#include <random>
-#include <span>
-#include <thread>
-#include <typeindex>
-#include <unordered_map>
-
-#if ZEN_PLATFORM_WINDOWS
-# include <ppl.h>
-# include <process.h>
-#endif
-
-#include <zencore/memory/newdelete.h>
-
-//////////////////////////////////////////////////////////////////////////
-
#if ZEN_WITH_TESTS
-# define ZEN_TEST_WITH_RUNNER 1
-# include <zencore/testing.h>
-# include <zencore/workthreadpool.h>
-#endif
-
-using namespace std::literals;
-#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+# define ZEN_TEST_WITH_RUNNER 1
+# include "zenserver-test.h"
+
+# include <zencore/except.h>
+# include <zencore/fmtutils.h>
+# include <zencore/logging.h>
+# include <zencore/stream.h>
+# include <zencore/string.h>
+# include <zencore/testutils.h>
+# include <zencore/thread.h>
+# include <zencore/timer.h>
+# include <zenhttp/httpclient.h>
+# include <zenhttp/packageformat.h>
+# include <zenutil/logging/testformatter.h>
+# include <zenutil/zenserverprocess.h>
+
+# include <atomic>
+# include <filesystem>
+
+# if ZEN_PLATFORM_WINDOWS
+# include <ppl.h>
+# include <process.h>
+# else
+# include <thread>
struct Concurrency
{
template<typename... T>
@@ -85,12 +44,19 @@ struct Concurrency
}
}
};
-#endif
+# endif
+
+# include <zencore/memory/newdelete.h>
//////////////////////////////////////////////////////////////////////////
-#if ZEN_WITH_TESTS
+using namespace std::literals;
+
+//////////////////////////////////////////////////////////////////////////
+
+namespace zen::tests {
zen::ZenServerEnvironment TestEnv;
+}
int
main(int argc, char** argv)
@@ -136,7 +102,7 @@ main(int argc, char** argv)
}
}
- TestEnv.InitializeForTest(ProgramBaseDir, TestBaseDir, ServerClass);
+ zen::tests::TestEnv.InitializeForTest(ProgramBaseDir, TestBaseDir, ServerClass);
ZEN_INFO("Running tests...(base dir: '{}')", TestBaseDir);
@@ -148,16 +114,6 @@ main(int argc, char** argv)
namespace zen::tests {
-IoBuffer
-MakeCbObjectPayload(std::function<void(CbObjectWriter& Writer)> WriteCB)
-{
- CbObjectWriter Writer;
- WriteCB(Writer);
- IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer();
- Payload.SetContentType(ZenContentType::kCbObject);
- return Payload;
-};
-
TEST_CASE("default.single")
{
std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
@@ -292,2749 +248,6 @@ TEST_CASE("multi.basic")
zen::NiceRate(RequestCount, (uint32_t)Elapsed, "req"));
}
-TEST_CASE("project.basic")
-{
- using namespace std::literals;
-
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
-
- ZenServerInstance Instance1(TestEnv);
- Instance1.SetTestDir(TestDir);
-
- const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
-
- std::mt19937_64 mt;
-
- zen::StringBuilder<64> BaseUri;
- BaseUri << fmt::format("http://localhost:{}", PortNumber);
-
- std::filesystem::path BinPath = zen::GetRunningExecutablePath();
- std::filesystem::path RootPath = BinPath.parent_path().parent_path();
- BinPath = BinPath.lexically_relative(RootPath);
-
- SUBCASE("build store init")
- {
- {
- HttpClient Http{BaseUri};
-
- {
- zen::CbObjectWriter Body;
- Body << "id"
- << "test";
- Body << "root" << RootPath.c_str();
- Body << "project"
- << "/zooom";
- Body << "engine"
- << "/zooom";
-
- zen::BinaryWriter MemOut;
- IoBuffer BodyBuf = Body.Save().GetBuffer().AsIoBuffer();
-
- auto Response = Http.Post("/prj/test"sv, BodyBuf);
- CHECK(Response.StatusCode == HttpResponseCode::Created);
- }
-
- {
- auto Response = Http.Get("/prj/test"sv);
- CHECK(Response.StatusCode == HttpResponseCode::OK);
-
- CbObject ResponseObject = Response.AsObject();
-
- CHECK(ResponseObject["id"].AsString() == "test"sv);
- CHECK(ResponseObject["root"].AsString() == PathToUtf8(RootPath.c_str()));
- }
- }
-
- BaseUri << "/prj/test/oplog/foobar";
-
- {
- HttpClient Http{BaseUri};
-
- {
- auto Response = Http.Post(""sv);
- CHECK(Response.StatusCode == HttpResponseCode::Created);
- }
-
- {
- auto Response = Http.Get(""sv);
- CHECK(Response.StatusCode == HttpResponseCode::OK);
-
- CbObject ResponseObject = Response.AsObject();
-
- CHECK(ResponseObject["id"].AsString() == "foobar"sv);
- CHECK(ResponseObject["project"].AsString() == "test"sv);
- }
- }
-
- SUBCASE("build store persistence")
- {
- uint8_t AttachData[] = {1, 2, 3};
-
- zen::CompressedBuffer Attachment = zen::CompressedBuffer::Compress(zen::SharedBuffer::Clone(zen::MemoryView{AttachData, 3}));
- zen::CbAttachment Attach{Attachment, Attachment.DecodeRawHash()};
-
- zen::CbObjectWriter OpWriter;
- OpWriter << "key"
- << "foo"
- << "attachment" << Attach;
-
- const std::string_view ChunkId{
- "00000000"
- "00000000"
- "00010000"};
- auto FileOid = zen::Oid::FromHexString(ChunkId);
-
- OpWriter.BeginArray("files");
- OpWriter.BeginObject();
- OpWriter << "id" << FileOid;
- OpWriter << "clientpath"
- << "/{engine}/client/side/path";
- OpWriter << "serverpath" << BinPath.c_str();
- OpWriter.EndObject();
- OpWriter.EndArray();
-
- zen::CbObject Op = OpWriter.Save();
-
- zen::CbPackage OpPackage(Op);
- OpPackage.AddAttachment(Attach);
-
- zen::BinaryWriter MemOut;
- legacy::SaveCbPackage(OpPackage, MemOut);
-
- HttpClient Http{BaseUri};
-
- {
- auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView()));
-
- REQUIRE(Response);
- CHECK(Response.StatusCode == HttpResponseCode::Created);
- }
-
- // Read file data
-
- {
- zen::StringBuilder<128> ChunkGetUri;
- ChunkGetUri << "/" << ChunkId;
- auto Response = Http.Get(ChunkGetUri);
-
- REQUIRE(Response);
- CHECK(Response.StatusCode == HttpResponseCode::OK);
- }
-
- {
- zen::StringBuilder<128> ChunkGetUri;
- ChunkGetUri << "/" << ChunkId << "?offset=1&size=10";
- auto Response = Http.Get(ChunkGetUri);
-
- REQUIRE(Response);
- CHECK(Response.StatusCode == HttpResponseCode::OK);
- CHECK(Response.ResponsePayload.GetSize() == 10);
- }
-
- ZEN_INFO("+++++++");
- }
-
- SUBCASE("snapshot")
- {
- zen::CbObjectWriter OpWriter;
- OpWriter << "key"
- << "foo";
-
- const std::string_view ChunkId{
- "00000000"
- "00000000"
- "00010000"};
- auto FileOid = zen::Oid::FromHexString(ChunkId);
-
- OpWriter.BeginArray("files");
- OpWriter.BeginObject();
- OpWriter << "id" << FileOid;
- OpWriter << "clientpath"
- << "/{engine}/client/side/path";
- OpWriter << "serverpath" << BinPath.c_str();
- OpWriter.EndObject();
- OpWriter.EndArray();
-
- zen::CbObject Op = OpWriter.Save();
-
- zen::CbPackage OpPackage(Op);
-
- zen::BinaryWriter MemOut;
- legacy::SaveCbPackage(OpPackage, MemOut);
-
- HttpClient Http{BaseUri};
-
- {
- auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView()));
-
- REQUIRE(Response);
- CHECK(Response.StatusCode == HttpResponseCode::Created);
- }
-
- // Read file data, it is raw and uncompressed
- {
- zen::StringBuilder<128> ChunkGetUri;
- ChunkGetUri << "/" << ChunkId;
- auto Response = Http.Get(ChunkGetUri);
-
- REQUIRE(Response);
- CHECK(Response.StatusCode == HttpResponseCode::OK);
-
- IoBuffer Data = Response.ResponsePayload;
- IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath);
- CHECK(ReferenceData.GetSize() == Data.GetSize());
- CHECK(ReferenceData.GetView().EqualBytes(Data.GetView()));
- }
-
- {
- IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); });
- auto Response = Http.Post("/rpc"sv, Payload, {{"Content-Type", "application/x-ue-cb"}});
- REQUIRE(Response);
- CHECK(Response.StatusCode == HttpResponseCode::OK);
- }
-
- // Read chunk data, it is now compressed
- {
- zen::StringBuilder<128> ChunkGetUri;
- ChunkGetUri << "/" << ChunkId;
- auto Response = Http.Get(ChunkGetUri, {{"Accept-Type", "application/x-ue-comp"}});
-
- REQUIRE(Response);
- CHECK(Response.StatusCode == HttpResponseCode::OK);
-
- IoBuffer Data = Response.ResponsePayload;
- IoHash RawHash;
- uint64_t RawSize;
- CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize);
- CHECK(Compressed);
- IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer();
- IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath);
- CHECK(RawSize == ReferenceData.GetSize());
- CHECK(ReferenceData.GetSize() == DataDecompressed.GetSize());
- CHECK(ReferenceData.GetView().EqualBytes(DataDecompressed.GetView()));
- }
-
- ZEN_INFO("+++++++");
- }
-
- SUBCASE("test chunk not found error")
- {
- HttpClient Http{BaseUri};
-
- for (size_t I = 0; I < 65; I++)
- {
- zen::StringBuilder<128> PostUri;
- PostUri << "/f77c781846caead318084604/info";
- auto Response = Http.Get(PostUri);
-
- REQUIRE(!Response.Error);
- CHECK(Response.StatusCode == HttpResponseCode::NotFound);
- }
- }
- }
-}
-
-namespace utils {
-
- struct ZenConfig
- {
- std::filesystem::path DataDir;
- uint16_t Port;
- std::string BaseUri;
- std::string Args;
-
- static ZenConfig New(std::string Args = "")
- {
- return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = TestEnv.GetNewPortNumber(), .Args = std::move(Args)};
- }
-
- static ZenConfig New(uint16_t Port, std::string Args = "")
- {
- return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = Port, .Args = std::move(Args)};
- }
-
- static ZenConfig NewWithUpstream(uint16_t Port, uint16_t UpstreamPort, std::string Args = "")
- {
- return New(Port,
- fmt::format("{}{}--debug --upstream-thread-count=0 --upstream-zen-url=http://localhost:{}",
- Args,
- Args.length() > 0 ? " " : "",
- UpstreamPort));
- }
-
- static ZenConfig NewWithThreadedUpstreams(uint16_t NewPort, std::span<uint16_t> UpstreamPorts, bool Debug)
- {
- std::string Args = Debug ? "--debug" : "";
- for (uint16_t Port : UpstreamPorts)
- {
- Args = fmt::format("{}{}--upstream-zen-url=http://localhost:{}", Args, Args.length() > 0 ? " " : "", Port);
- }
- return New(NewPort, Args);
- }
-
- void Spawn(ZenServerInstance& Inst)
- {
- Inst.SetTestDir(DataDir);
- Inst.SpawnServer(Port, Args);
- const uint16_t InstancePort = Inst.WaitUntilReady();
- CHECK_MESSAGE(InstancePort != 0, Inst.GetLogOutput());
-
- if (Port != InstancePort)
- ZEN_DEBUG("relocation detected from {} to {}", Port, InstancePort);
-
- Port = InstancePort;
- BaseUri = fmt::format("http://localhost:{}/z$", Port);
- }
- };
-
- void SpawnServer(ZenServerInstance& Server, ZenConfig& Cfg) { Cfg.Spawn(Server); }
-
- CompressedBuffer CreateSemiRandomBlob(size_t AttachmentSize, OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast)
- {
- // Convoluted way to get a compressed buffer whose result it large enough to be a separate file
- // but also does actually compress
- const size_t PartCount = (AttachmentSize / (1u * 1024u * 64)) + 1;
- const size_t PartSize = AttachmentSize / PartCount;
- auto Part = SharedBuffer(CreateRandomBlob(PartSize));
- std::vector<SharedBuffer> Parts(PartCount, Part);
- size_t RemainPartSize = AttachmentSize - (PartSize * PartCount);
- if (RemainPartSize > 0)
- {
- Parts.push_back(SharedBuffer(CreateRandomBlob(RemainPartSize)));
- }
- CompressedBuffer Value = CompressedBuffer::Compress(CompositeBuffer(std::move(Parts)), OodleCompressor::Mermaid, CompressionLevel);
- return Value;
- };
-
- std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments(const std::span<const size_t>& Sizes)
- {
- std::vector<std::pair<Oid, CompressedBuffer>> Result;
- Result.reserve(Sizes.size());
- for (size_t Size : Sizes)
- {
- CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(CreateRandomBlob(Size)));
- Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed));
- }
- return Result;
- }
-
- std::vector<std::pair<Oid, CompressedBuffer>> CreateSemiRandomAttachments(const std::span<const size_t>& Sizes)
- {
- std::vector<std::pair<Oid, CompressedBuffer>> Result;
- Result.reserve(Sizes.size());
- for (size_t Size : Sizes)
- {
- CompressedBuffer Compressed =
- CreateSemiRandomBlob(Size, Size > 1024u * 1024u ? OodleCompressionLevel::None : OodleCompressionLevel::VeryFast);
- Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed));
- }
- return Result;
- }
-
-} // namespace utils
-
-TEST_CASE("zcache.basic")
-{
- using namespace std::literals;
-
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
-
- const int kIterationCount = 100;
-
- auto HashKey = [](int i) -> zen::IoHash { return zen::IoHash::HashBuffer(&i, sizeof i); };
-
- {
- ZenServerInstance Instance1(TestEnv);
- Instance1.SetTestDir(TestDir);
-
- const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
- const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber);
-
- // Populate with some simple data
-
- HttpClient Http{BaseUri};
-
- for (int i = 0; i < kIterationCount; ++i)
- {
- zen::CbObjectWriter Cbo;
- Cbo << "index" << i;
-
- IoBuffer Payload = Cbo.Save().GetBuffer().AsIoBuffer();
- Payload.SetContentType(HttpContentType::kCbObject);
-
- zen::IoHash Key = HashKey(i);
-
- HttpClient::Response Result = Http.Put(fmt::format("/test/{}", Key), Payload);
-
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- // Retrieve data
-
- for (int i = 0; i < kIterationCount; ++i)
- {
- zen::IoHash Key = HashKey(i);
-
- HttpClient::Response Result = Http.Get(fmt::format("/test/{}", Key), {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
-
- // Ensure bad bucket identifiers are rejected
-
- {
- zen::CbObjectWriter Cbo;
- Cbo << "index" << 42;
-
- IoBuffer Payload = Cbo.Save().GetBuffer().AsIoBuffer();
- Payload.SetContentType(HttpContentType::kCbObject);
-
- zen::IoHash Key = HashKey(442);
-
- HttpClient::Response Result = Http.Put(fmt::format("/te!st/{}", Key), Payload);
-
- CHECK(Result.StatusCode == HttpResponseCode::BadRequest);
- }
- }
-
- // Verify that the data persists between process runs (the previous server has exited at this point)
-
- {
- ZenServerInstance Instance1(TestEnv);
- Instance1.SetTestDir(TestDir);
- const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
-
- const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber);
-
- HttpClient Http{BaseUri};
-
- // Retrieve data again
-
- for (int i = 0; i < kIterationCount; ++i)
- {
- zen::IoHash Key = HashKey(i);
-
- HttpClient::Response Result = Http.Get(fmt::format("/{}/{}", "test", Key), {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
- }
-}
-
-IoBuffer
-SerializeToBuffer(const zen::CbPackage& Package)
-{
- BinaryWriter MemStream;
-
- Package.Save(MemStream);
-
- IoBuffer Buffer = zen::IoBuffer(zen::IoBuffer::Clone, MemStream.Data(), MemStream.Size());
- Buffer.SetContentType(HttpContentType::kCbPackage);
- return Buffer;
-};
-
-TEST_CASE("zcache.cbpackage")
-{
- using namespace std::literals;
-
- auto CreateTestPackage = [](zen::IoHash& OutAttachmentKey) -> zen::CbPackage {
- auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9}));
- auto CompressedData = zen::CompressedBuffer::Compress(Data);
-
- OutAttachmentKey = CompressedData.DecodeRawHash();
-
- zen::CbWriter Obj;
- Obj.BeginObject("obj"sv);
- Obj.AddBinaryAttachment("data", OutAttachmentKey);
- Obj.EndObject();
-
- zen::CbPackage Package;
- Package.SetObject(Obj.Save().AsObject());
- Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey));
-
- return Package;
- };
-
- auto IsEqual = [](zen::CbPackage Lhs, zen::CbPackage Rhs) -> bool {
- std::span<const zen::CbAttachment> LhsAttachments = Lhs.GetAttachments();
- std::span<const zen::CbAttachment> RhsAttachments = Rhs.GetAttachments();
-
- if (LhsAttachments.size() != RhsAttachments.size())
- {
- return false;
- }
-
- for (const zen::CbAttachment& LhsAttachment : LhsAttachments)
- {
- const zen::CbAttachment* RhsAttachment = Rhs.FindAttachment(LhsAttachment.GetHash());
- CHECK(RhsAttachment);
-
- zen::SharedBuffer LhsBuffer = LhsAttachment.AsCompressedBinary().Decompress();
- CHECK(!LhsBuffer.IsNull());
-
- zen::SharedBuffer RhsBuffer = RhsAttachment->AsCompressedBinary().Decompress();
- CHECK(!RhsBuffer.IsNull());
-
- if (!LhsBuffer.GetView().EqualBytes(RhsBuffer.GetView()))
- {
- return false;
- }
- }
-
- return true;
- };
-
- SUBCASE("PUT/GET returns correct package")
- {
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
-
- ZenServerInstance Instance1(TestEnv);
- Instance1.SetTestDir(TestDir);
- const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady();
- const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber);
-
- HttpClient Http{BaseUri};
-
- const std::string_view Bucket = "mosdef"sv;
- zen::IoHash Key;
- zen::CbPackage ExpectedPackage = CreateTestPackage(Key);
-
- // PUT
- {
- zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage);
- HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), Body);
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- // GET
- {
- HttpClient::Response Result = Http.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
-
- zen::CbPackage Package;
- const bool Ok = Package.TryLoad(Result.ResponsePayload);
- CHECK(Ok);
- CHECK(IsEqual(Package, ExpectedPackage));
- }
- }
-
- SUBCASE("PUT propagates upstream")
- {
- // Setup local and remote server
- std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir();
- std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir();
-
- ZenServerInstance RemoteInstance(TestEnv);
- RemoteInstance.SetTestDir(RemoteDataDir);
- const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady();
-
- ZenServerInstance LocalInstance(TestEnv);
- LocalInstance.SetTestDir(LocalDataDir);
- LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(),
- fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber));
- const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady();
- CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput());
-
- const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber);
- const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber);
-
- const std::string_view Bucket = "mosdef"sv;
- zen::IoHash Key;
- zen::CbPackage ExpectedPackage = CreateTestPackage(Key);
-
- HttpClient LocalHttp{LocalBaseUri};
- HttpClient RemoteHttp{RemoteBaseUri};
-
- // Store the cache record package in the local instance
- {
- zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage);
- HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}", Bucket, Key), Body);
-
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- // The cache record can be retrieved as a package from the local instance
- {
- HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
-
- zen::CbPackage Package;
- const bool Ok = Package.TryLoad(Result.ResponsePayload);
- CHECK(Ok);
- CHECK(IsEqual(Package, ExpectedPackage));
- }
-
- // The cache record can be retrieved as a package from the remote instance
- {
- HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
-
- zen::CbPackage Package;
- const bool Ok = Package.TryLoad(Result.ResponsePayload);
- CHECK(Ok);
- CHECK(IsEqual(Package, ExpectedPackage));
- }
- }
-
- SUBCASE("GET finds upstream when missing in local")
- {
- // Setup local and remote server
- std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir();
- std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir();
-
- ZenServerInstance RemoteInstance(TestEnv);
- RemoteInstance.SetTestDir(RemoteDataDir);
- const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady();
-
- ZenServerInstance LocalInstance(TestEnv);
- LocalInstance.SetTestDir(LocalDataDir);
- LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(),
- fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber));
- const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady();
- CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput());
-
- const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber);
- const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber);
-
- HttpClient LocalHttp{LocalBaseUri};
- HttpClient RemoteHttp{RemoteBaseUri};
-
- const std::string_view Bucket = "mosdef"sv;
- zen::IoHash Key;
- zen::CbPackage ExpectedPackage = CreateTestPackage(Key);
-
- // Store the cache record package in upstream cache
- {
- zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage);
- HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), Body);
-
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- // The cache record can be retrieved as a package from the local cache
- {
- HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
-
- zen::CbPackage Package;
- const bool Ok = Package.TryLoad(Result.ResponsePayload);
- CHECK(Ok);
- CHECK(IsEqual(Package, ExpectedPackage));
- }
- }
-}
-
-TEST_CASE("zcache.policy")
-{
- using namespace std::literals;
- using namespace utils;
-
- auto GenerateData = [](uint64_t Size, zen::IoHash& OutHash) -> zen::IoBuffer {
- auto Buf = zen::UniqueBuffer::Alloc(Size);
- uint8_t* Data = reinterpret_cast<uint8_t*>(Buf.GetData());
- for (uint64_t Idx = 0; Idx < Size; Idx++)
- {
- Data[Idx] = Idx % 256;
- }
- OutHash = zen::IoHash::HashBuffer(Data, Size);
- return Buf.MoveToShared().AsIoBuffer();
- };
-
- auto GeneratePackage = [](zen::IoHash& OutRecordKey, zen::IoHash& OutAttachmentKey) -> zen::CbPackage {
- auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9}));
- auto CompressedData = zen::CompressedBuffer::Compress(Data);
- OutAttachmentKey = CompressedData.DecodeRawHash();
-
- zen::CbWriter Writer;
- Writer.BeginObject("obj"sv);
- Writer.AddBinaryAttachment("data", OutAttachmentKey);
- Writer.EndObject();
- CbObject CacheRecord = Writer.Save().AsObject();
-
- OutRecordKey = IoHash::HashBuffer(CacheRecord.GetBuffer().GetView());
-
- zen::CbPackage Package;
- Package.SetObject(CacheRecord);
- Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey));
-
- return Package;
- };
-
- SUBCASE("query - 'local' does not query upstream (binary)")
- {
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamInst(TestEnv);
- UpstreamCfg.Spawn(UpstreamInst);
- const uint16_t UpstreamPort = UpstreamCfg.Port;
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort);
- ZenServerInstance LocalInst(TestEnv);
- LocalCfg.Spawn(LocalInst);
-
- const std::string_view Bucket = "legacy"sv;
-
- zen::IoHash Key;
- IoBuffer BinaryValue = GenerateData(1024, Key);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient RemoteHttp{UpstreamCfg.BaseUri};
-
- {
- HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), BinaryValue);
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- {
- HttpClient::Response Result =
- LocalHttp.Get(fmt::format("/{}/{}?Policy=QueryLocal,Store", Bucket, Key), {{"Accept", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::NotFound);
- }
-
- {
- HttpClient::Response Result =
- LocalHttp.Get(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), {{"Accept", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
- }
-
- SUBCASE("store - 'local' does not store upstream (binary)")
- {
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamInst(TestEnv);
- UpstreamCfg.Spawn(UpstreamInst);
- const uint16_t UpstreamPort = UpstreamCfg.Port;
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort);
- ZenServerInstance LocalInst(TestEnv);
- LocalCfg.Spawn(LocalInst);
-
- const auto Bucket = "legacy"sv;
-
- zen::IoHash Key;
- IoBuffer BinaryValue = GenerateData(1024, Key);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient RemoteHttp{UpstreamCfg.BaseUri};
-
- // Store binary cache value locally
- {
- HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,StoreLocal", Bucket, Key),
- BinaryValue,
- {{"Content-Type", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- {
- HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::NotFound);
- }
-
- {
- HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
- }
-
- SUBCASE("store - 'local/remote' stores local and upstream (binary)")
- {
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamInst(TestEnv);
- UpstreamCfg.Spawn(UpstreamInst);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalInst(TestEnv);
- LocalCfg.Spawn(LocalInst);
-
- const auto Bucket = "legacy"sv;
-
- zen::IoHash Key;
- IoBuffer BinaryValue = GenerateData(1024, Key);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient RemoteHttp{UpstreamCfg.BaseUri};
-
- // Store binary cache value locally and upstream
- {
- HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key),
- BinaryValue,
- {{"Content-Type", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- {
- HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
-
- {
- HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
- }
-
- SUBCASE("query - 'local' does not query upstream (cbpackage)")
- {
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamInst(TestEnv);
- UpstreamCfg.Spawn(UpstreamInst);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalInst(TestEnv);
- LocalCfg.Spawn(LocalInst);
-
- const auto Bucket = "legacy"sv;
-
- zen::IoHash Key;
- zen::IoHash PayloadId;
- zen::CbPackage Package = GeneratePackage(Key, PayloadId);
- IoBuffer Buf = SerializeToBuffer(Package);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient RemoteHttp{UpstreamCfg.BaseUri};
-
- // Store package upstream
- {
- HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), Buf);
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- {
- HttpClient::Response Result =
- LocalHttp.Get(fmt::format("/{}/{}?Policy=QueryLocal,Store", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::NotFound);
- }
-
- {
- HttpClient::Response Result =
- LocalHttp.Get(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
- }
-
- SUBCASE("store - 'local' does not store upstream (cbpackage)")
- {
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamInst(TestEnv);
- UpstreamCfg.Spawn(UpstreamInst);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalInst(TestEnv);
- LocalCfg.Spawn(LocalInst);
-
- const auto Bucket = "legacy"sv;
-
- zen::IoHash Key;
- zen::IoHash PayloadId;
- zen::CbPackage Package = GeneratePackage(Key, PayloadId);
- IoBuffer Buf = SerializeToBuffer(Package);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient RemoteHttp{UpstreamCfg.BaseUri};
-
- // Store package locally
- {
- HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,StoreLocal", Bucket, Key), Buf);
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- {
- HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::NotFound);
- }
-
- {
- HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
- }
-
- SUBCASE("store - 'local/remote' stores local and upstream (cbpackage)")
- {
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamInst(TestEnv);
- UpstreamCfg.Spawn(UpstreamInst);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalInst(TestEnv);
- LocalCfg.Spawn(LocalInst);
-
- const auto Bucket = "legacy"sv;
-
- zen::IoHash Key;
- zen::IoHash PayloadId;
- zen::CbPackage Package = GeneratePackage(Key, PayloadId);
- IoBuffer Buf = SerializeToBuffer(Package);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient RemoteHttp{UpstreamCfg.BaseUri};
-
- // Store package locally and upstream
- {
- HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), Buf);
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- {
- HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
-
- {
- HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- }
- }
-
- SUBCASE("skip - 'data' returns cache record without attachments/empty payload")
- {
- ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance Instance(TestEnv);
- Cfg.Spawn(Instance);
-
- const auto Bucket = "test"sv;
-
- zen::IoHash Key;
- zen::IoHash PayloadId;
- zen::CbPackage Package = GeneratePackage(Key, PayloadId);
- IoBuffer Buf = SerializeToBuffer(Package);
-
- HttpClient Http{Cfg.BaseUri};
-
- // Store package
- {
- HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), Buf);
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- // Get package
- {
- HttpClient::Response Result =
- Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Result);
- CbPackage ResponsePackage;
- CHECK(ResponsePackage.TryLoad(Result.ResponsePayload));
- CHECK(ResponsePackage.GetAttachments().size() == 0);
- }
-
- // Get record
- {
- HttpClient::Response Result =
- Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/x-ue-cb"}});
- CHECK(Result);
- CbObject ResponseObject = zen::LoadCompactBinaryObject(Result.ResponsePayload);
- CHECK(ResponseObject);
- }
-
- // Get payload
- {
- HttpClient::Response Result =
- Http.Get(fmt::format("/{}/{}/{}?Policy=Default,SkipData", Bucket, Key, PayloadId), {{"Accept", "application/x-ue-comp"}});
- CHECK(Result);
- CHECK(Result.ResponsePayload.GetSize() == 0);
- }
- }
-
- SUBCASE("skip - 'data' returns empty binary value")
- {
- ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance Instance(TestEnv);
- Cfg.Spawn(Instance);
-
- const auto Bucket = "test"sv;
-
- zen::IoHash Key;
- IoBuffer BinaryValue = GenerateData(1024, Key);
-
- HttpClient Http{Cfg.BaseUri};
-
- // Store binary cache value
- {
- HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), BinaryValue);
- CHECK(Result.StatusCode == HttpResponseCode::Created);
- }
-
- // Get package
- {
- HttpClient::Response Result =
- Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/octet-stream"}});
- CHECK(Result);
- CHECK(Result.ResponsePayload.GetSize() == 0);
- }
- }
-}
-
-TEST_CASE("zcache.rpc")
-{
- using namespace std::literals;
-
- auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request,
- const zen::CacheKey& CacheKey,
- size_t PayloadSize,
- CachePolicy RecordPolicy) {
- std::vector<uint8_t> Data;
- Data.resize(PayloadSize);
- uint32_t DataSeed = *reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0]);
- uint16_t* DataPtr = reinterpret_cast<uint16_t*>(Data.data());
- for (size_t Idx = 0; Idx < PayloadSize / 2; ++Idx)
- {
- DataPtr[Idx] = static_cast<uint16_t>((Idx + DataSeed) % 0xffffu);
- }
- if (PayloadSize & 1)
- {
- Data[PayloadSize - 1] = static_cast<uint8_t>((PayloadSize - 1) & 0xff);
- }
- CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size()));
- Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy});
- };
-
- auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri,
- std::string_view Namespace,
- std::string_view Bucket,
- size_t Num,
- size_t PayloadSize = 1024,
- size_t KeyOffset = 1,
- CachePolicy PutPolicy = CachePolicy::Default,
- std::vector<CbPackage>* OutPackages = nullptr) -> std::vector<CacheKey> {
- std::vector<zen::CacheKey> OutKeys;
-
- HttpClient Http{BaseUri};
-
- for (uint32_t Key = 1; Key <= Num; ++Key)
- {
- zen::IoHash KeyHash;
- ((uint32_t*)(KeyHash.Hash))[0] = gsl::narrow<uint32_t>(KeyOffset + Key);
- const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash);
-
- cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
- AppendCacheRecord(Request, CacheKey, PayloadSize, PutPolicy);
- OutKeys.push_back(CacheKey);
-
- CbPackage Package;
- CHECK(Request.Format(Package));
-
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- if (OutPackages)
- {
- OutPackages->emplace_back(std::move(Package));
- }
- }
-
- return OutKeys;
- };
-
- struct GetCacheRecordResult
- {
- zen::CbPackage Response;
- cacherequests::GetCacheRecordsResult Result;
- bool Success;
- };
-
- auto GetCacheRecords = [](std::string_view BaseUri,
- std::string_view Namespace,
- std::span<zen::CacheKey> Keys,
- zen::CachePolicy Policy,
- zen::RpcAcceptOptions AcceptOptions = zen::RpcAcceptOptions::kNone,
- int Pid = 0) -> GetCacheRecordResult {
- cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
- .AcceptOptions = static_cast<uint16_t>(AcceptOptions),
- .ProcessPid = Pid,
- .DefaultPolicy = Policy,
- .Namespace = std::string(Namespace)};
- for (const CacheKey& Key : Keys)
- {
- Request.Requests.push_back({.Key = Key});
- }
-
- CbObjectWriter RequestWriter;
- CHECK(Request.Format(RequestWriter));
-
- IoBuffer Body = RequestWriter.Save().GetBuffer().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbObject);
-
- HttpClient Http{BaseUri};
-
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- GetCacheRecordResult OutResult;
-
- if (Result.StatusCode == HttpResponseCode::OK)
- {
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- CHECK(!Response.IsNull());
- OutResult.Response = std::move(Response);
- CHECK(OutResult.Result.Parse(OutResult.Response));
- OutResult.Success = true;
- }
-
- return OutResult;
- };
-
- SUBCASE("get cache records")
- {
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
-
- ZenServerInstance Inst(TestEnv);
- Inst.SetTestDir(TestDir);
-
- const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady();
- const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort);
-
- CachePolicy Policy = CachePolicy::Default;
- std::vector<zen::CacheKey> Keys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 128);
- GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record);
- CHECK(Record->Key == ExpectedKey);
- CHECK(Record->Values.size() == 1);
-
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- CHECK(Value.Body);
- }
- }
- }
-
- SUBCASE("get missing cache records")
- {
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
-
- ZenServerInstance Inst(TestEnv);
- Inst.SetTestDir(TestDir);
- const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady();
- const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort);
-
- CachePolicy Policy = CachePolicy::Default;
- std::vector<zen::CacheKey> ExistingKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 128);
- std::vector<zen::CacheKey> Keys;
-
- for (const zen::CacheKey& Key : ExistingKeys)
- {
- Keys.push_back(Key);
- Keys.push_back(CacheKey::Create("missing"sv, IoHash::Zero));
- }
-
- GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- size_t KeyIndex = 0;
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- const bool Missing = Index++ % 2 != 0;
-
- if (Missing)
- {
- CHECK(!Record);
- }
- else
- {
- const CacheKey& ExpectedKey = ExistingKeys[KeyIndex++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- CHECK(Value.Body);
- }
- }
- }
- }
-
- SUBCASE("policy - 'QueryLocal' does not query upstream")
- {
- using namespace utils;
-
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamServer(TestEnv);
- SpawnServer(UpstreamServer, UpstreamCfg);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4);
-
- CachePolicy Policy = CachePolicy::QueryLocal;
- GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(!Record);
- }
- }
-
- SUBCASE("policy - 'QueryRemote' does query upstream")
- {
- using namespace utils;
-
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamServer(TestEnv);
- SpawnServer(UpstreamServer, UpstreamCfg);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4);
-
- CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
- GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- }
- }
-
- SUBCASE("policy - 'QueryLocal' on put allows overwrite with differing value when not limiting overwrites")
- {
- using namespace utils;
-
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamServer(TestEnv);
- SpawnServer(UpstreamServer, UpstreamCfg);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- size_t PayloadSize = 1024;
- std::string_view Namespace("ue4.ddc"sv);
- std::string_view Bucket("mastodon"sv);
- const size_t NumRecords = 4;
- std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
-
- for (const zen::CacheKey& CacheKey : Keys)
- {
- cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
- AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default);
-
- CbPackage Package;
- CHECK(Request.Format(Package));
-
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
- HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- cacherequests::PutCacheRecordsResult ParsedResult;
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- CHECK(!Response.IsNull());
- CHECK(ParsedResult.Parse(Response));
- for (bool ResponseSuccess : ParsedResult.Success)
- {
- CHECK(ResponseSuccess);
- }
- CHECK(ParsedResult.Details.empty());
- }
-
- auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
- CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
- GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- CHECK(Value.RawSize == PayloadSize * 2);
- }
- }
- };
-
- // Check that the records are present and overwritten in the local server
- CheckRecordCorrectness(LocalCfg);
-
- // Check that the records are present and overwritten in the upstream server
- CheckRecordCorrectness(UpstreamCfg);
- }
-
- SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites")
- {
- using namespace utils;
-
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamServer(TestEnv);
- SpawnServer(UpstreamServer, UpstreamCfg);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites");
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- size_t PayloadSize = 1024;
- std::string_view Namespace("ue4.ddc"sv);
- std::string_view Bucket("mastodon"sv);
- const size_t NumRecords = 4;
- std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
-
- for (const zen::CacheKey& CacheKey : Keys)
- {
- cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
- AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default);
-
- CbPackage Package;
- CHECK(Request.Format(Package));
-
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
-
- HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- cacherequests::PutCacheRecordsResult ParsedResult;
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- CHECK(!Response.IsNull());
- CHECK(ParsedResult.Parse(Response));
- CHECK(Request.Requests.size() == ParsedResult.Success.size());
- for (bool ResponseSuccess : ParsedResult.Success)
- {
- CHECK(ResponseSuccess);
- }
- CHECK(Request.Requests.size() == ParsedResult.Details.size());
- for (const CbObjectView& Details : ParsedResult.Details)
- {
- CHECK(Details);
- CHECK(Details["RawHash"sv].IsHash());
- CHECK(Details["RawSize"sv].IsInteger());
- CHECK(Details["Record"sv].IsObject());
- }
- }
-
- auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
- CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
- GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- CHECK(Value.RawSize == PayloadSize);
- }
- }
- };
-
- // Check that the records are present and not overwritten in the local server
- CheckRecordCorrectness(LocalCfg);
-
- // Check that the records are present and not overwritten in the upstream server
- CheckRecordCorrectness(UpstreamCfg);
- }
-
- SUBCASE("policy - no 'QueryLocal' on put allows overwrite with differing value when limiting overwrites")
- {
- using namespace utils;
-
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamServer(TestEnv);
- SpawnServer(UpstreamServer, UpstreamCfg);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites");
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
-
- size_t PayloadSize = 1024;
- std::string_view Namespace("ue4.ddc"sv);
- std::string_view Bucket("mastodon"sv);
- const size_t NumRecords = 4;
- std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize);
-
- for (const zen::CacheKey& CacheKey : Keys)
- {
- cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
- AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Store);
-
- CbPackage Package;
- CHECK(Request.Format(Package));
-
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
- HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- cacherequests::PutCacheRecordsResult ParsedResult;
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- CHECK(!Response.IsNull());
- CHECK(ParsedResult.Parse(Response));
- for (bool ResponseSuccess : ParsedResult.Success)
- {
- CHECK(ResponseSuccess);
- }
- CHECK(ParsedResult.Details.empty());
- }
-
- auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
- CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
- GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- CHECK(Value.RawSize == PayloadSize * 2);
- }
- }
- };
-
- // Check that the records are present and overwritten in the local server
- CheckRecordCorrectness(LocalCfg);
-
- // Check that the records are present and overwritten in the upstream server
- CheckRecordCorrectness(UpstreamCfg);
- }
-
- SUBCASE("policy - 'QueryLocal' on put allows overwrite with equivalent value when limiting overwrites")
- {
- using namespace utils;
-
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamServer(TestEnv);
- SpawnServer(UpstreamServer, UpstreamCfg);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites");
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- HttpClient LocalHttp{LocalCfg.BaseUri};
- HttpClient UpstreamHttp{UpstreamCfg.BaseUri};
-
- size_t PayloadSize = 1024;
- std::string_view Namespace("ue4.ddc"sv);
- std::string_view Bucket("mastodon"sv);
- const size_t NumRecords = 4;
- std::vector<CbPackage> Packages;
- std::vector<zen::CacheKey> Keys =
- PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1, CachePolicy::Default, &Packages);
-
- for (const CbPackage& Package : Packages)
- {
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
- HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
- cacherequests::PutCacheRecordsResult ParsedResult;
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- CHECK(!Response.IsNull());
- CHECK(ParsedResult.Parse(Response));
- for (bool ResponseSuccess : ParsedResult.Success)
- {
- CHECK(ResponseSuccess);
- }
- CHECK(ParsedResult.Details.empty());
- }
-
- auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) {
- CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
- GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- CHECK(Value.RawSize == PayloadSize);
- }
- }
- };
-
- // Check that the records are present and unchanged in the local server
- CheckRecordCorrectness(LocalCfg);
-
- // Check that the records are present and unchanged in the upstream server
- CheckRecordCorrectness(UpstreamCfg);
- }
-
- // TODO: Propagation for rejected PUTs
- // SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites but allows propagation to
- // upstream")
- // {
- // using namespace utils;
-
- // ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- // ZenServerInstance UpstreamServer(TestEnv);
- // SpawnServer(UpstreamServer, UpstreamCfg);
-
- // ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port,
- // "--cache-bucket-limit-overwrites"); ZenServerInstance LocalServer(TestEnv); SpawnServer(LocalServer, LocalCfg);
-
- // size_t PayloadSize = 1024;
- // std::string_view Namespace("ue4.ddc"sv);
- // std::string_view Bucket("mastodon"sv);
- // const size_t NumRecords = 4;
- // std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1,
- // CachePolicy::Local);
-
- // for (const zen::CacheKey& CacheKey : Keys)
- // {
- // cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
- // AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default);
-
- // CbPackage Package;
- // CHECK(Request.Format(Package));
-
- // IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- // cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)},
- // cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}},
- // cpr::Body{(const char*)Body.GetData(), Body.GetSize()});
-
- // CHECK(Result.status_code == 200);
- // cacherequests::PutCacheRecordsResult ParsedResult;
- // CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size()));
- // CHECK(!Response.IsNull());
- // CHECK(ParsedResult.Parse(Response));
- // for (bool ResponseSuccess : ParsedResult.Success)
- // {
- // CHECK(!ResponseSuccess);
- // }
- // }
-
- // auto CheckRecordCorrectness = [&](const ZenConfig& Cfg, size_t ExpectedPayloadSize) {
- // CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote);
- // GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy);
-
- // CHECK(Result.Result.Results.size() == Keys.size());
-
- // for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- // {
- // CHECK(Record);
- // const CacheKey& ExpectedKey = Keys[Index++];
- // CHECK(Record->Key == ExpectedKey);
- // for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- // {
- // CHECK(Value.RawSize == ExpectedPayloadSize);
- // }
- // }
- // };
-
- // // Check that the records are present and not overwritten in the local server
- // CheckRecordCorrectness(LocalCfg, PayloadSize);
-
- // // Check that the records are present and are the newer size in the upstream server
- // CheckRecordCorrectness(UpstreamCfg, PayloadSize*2);
- // }
-
- SUBCASE("RpcAcceptOptions")
- {
- using namespace utils;
-
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
-
- ZenServerInstance Inst(TestEnv);
- Inst.SetTestDir(TestDir);
-
- const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady();
- const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort);
-
- std::vector<zen::CacheKey> SmallKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 4, 1024);
- std::vector<zen::CacheKey> LargeKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 4, 1024 * 1024 * 16, SmallKeys.size());
-
- std::vector<zen::CacheKey> Keys(SmallKeys.begin(), SmallKeys.end());
- Keys.insert(Keys.end(), LargeKeys.begin(), LargeKeys.end());
-
- {
- GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
- IoBufferFileReference Ref;
- bool IsFileRef = Body.GetFileReference(Ref);
- CHECK(!IsFileRef);
- }
- }
- }
-
- // File path, but only for large files
- {
- GetCacheRecordResult Result =
- GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default, RpcAcceptOptions::kAllowLocalReferences);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
- IoBufferFileReference Ref;
- bool IsFileRef = Body.GetFileReference(Ref);
- CHECK(IsFileRef == (Body.Size() > 1024));
- }
- }
- }
-
- // File path, for all files
- {
- GetCacheRecordResult Result =
- GetCacheRecords(BaseUri,
- "ue4.ddc"sv,
- Keys,
- CachePolicy::Default,
- RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences);
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
- IoBufferFileReference Ref;
- bool IsFileRef = Body.GetFileReference(Ref);
- CHECK(IsFileRef);
- }
- }
- }
-
- // File handle, but only for large files
- {
- GetCacheRecordResult Result = GetCacheRecords(BaseUri,
- "ue4.ddc"sv,
- Keys,
- CachePolicy::Default,
- RpcAcceptOptions::kAllowLocalReferences,
- GetCurrentProcessId());
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
- IoBufferFileReference Ref;
- bool IsFileRef = Body.GetFileReference(Ref);
- CHECK(IsFileRef == (Body.Size() > 1024));
- }
- }
- }
-
- // File handle, for all files
- {
- GetCacheRecordResult Result =
- GetCacheRecords(BaseUri,
- "ue4.ddc"sv,
- Keys,
- CachePolicy::Default,
- RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences,
- GetCurrentProcessId());
-
- CHECK(Result.Result.Results.size() == Keys.size());
-
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- CHECK(Record);
- const CacheKey& ExpectedKey = Keys[Index++];
- CHECK(Record->Key == ExpectedKey);
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer();
- IoBufferFileReference Ref;
- bool IsFileRef = Body.GetFileReference(Ref);
- CHECK(IsFileRef);
- }
- }
- }
- }
-}
-
-TEST_CASE("zcache.failing.upstream")
-{
- // This is an exploratory test that takes a long time to run, so lets skip it by default
- if (true)
- {
- return;
- }
-
- using namespace std::literals;
- using namespace utils;
-
- ZenConfig Upstream1Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance Upstream1Server(TestEnv);
- SpawnServer(Upstream1Server, Upstream1Cfg);
-
- ZenConfig Upstream2Cfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance Upstream2Server(TestEnv);
- SpawnServer(Upstream2Server, Upstream2Cfg);
-
- std::vector<std::uint16_t> UpstreamPorts = {Upstream1Cfg.Port, Upstream2Cfg.Port};
- ZenConfig LocalCfg = ZenConfig::NewWithThreadedUpstreams(TestEnv.GetNewPortNumber(), UpstreamPorts, false);
- LocalCfg.Args += (" --upstream-thread-count 2");
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- const uint16_t LocalPortNumber = LocalCfg.Port;
- const auto LocalUri = fmt::format("http://localhost:{}/z$", LocalPortNumber);
- const auto Upstream1Uri = fmt::format("http://localhost:{}/z$", Upstream1Cfg.Port);
- const auto Upstream2Uri = fmt::format("http://localhost:{}/z$", Upstream2Cfg.Port);
-
- bool Upstream1Running = true;
- bool Upstream2Running = true;
-
- using namespace std::literals;
-
- auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request,
- const zen::CacheKey& CacheKey,
- size_t PayloadSize,
- CachePolicy RecordPolicy) {
- std::vector<uint32_t> Data;
- Data.resize(PayloadSize / 4);
- for (uint32_t Idx = 0; Idx < PayloadSize / 4; ++Idx)
- {
- Data[Idx] = (*reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0])) + Idx;
- }
-
- CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size() * 4));
- Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy});
- };
-
- auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri,
- std::string_view Namespace,
- std::string_view Bucket,
- size_t Num,
- size_t KeyOffset,
- size_t PayloadSize = 8192) -> std::vector<CacheKey> {
- std::vector<zen::CacheKey> OutKeys;
-
- cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
- for (size_t Key = 1; Key <= Num; ++Key)
- {
- zen::IoHash KeyHash;
- ((size_t*)(KeyHash.Hash))[0] = KeyOffset + Key;
- const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash);
-
- AppendCacheRecord(Request, CacheKey, PayloadSize, CachePolicy::Default);
- OutKeys.push_back(CacheKey);
- }
-
- CbPackage Package;
- CHECK(Request.Format(Package));
-
- HttpClient Http{BaseUri};
-
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- if (Result.StatusCode != HttpResponseCode::OK)
- {
- ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage(""));
- OutKeys.clear();
- }
-
- return OutKeys;
- };
-
- struct GetCacheRecordResult
- {
- zen::CbPackage Response;
- cacherequests::GetCacheRecordsResult Result;
- bool Success = false;
- };
-
- auto GetCacheRecords = [](std::string_view BaseUri,
- std::string_view Namespace,
- std::span<zen::CacheKey> Keys,
- zen::CachePolicy Policy) -> GetCacheRecordResult {
- cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
- .DefaultPolicy = Policy,
- .Namespace = std::string(Namespace)};
- for (const CacheKey& Key : Keys)
- {
- Request.Requests.push_back({.Key = Key});
- }
-
- CbObjectWriter RequestWriter;
- CHECK(Request.Format(RequestWriter));
-
- IoBuffer Body = RequestWriter.Save().GetBuffer().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbObject);
-
- HttpClient Http{BaseUri};
-
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- GetCacheRecordResult OutResult;
-
- if (Result.StatusCode == HttpResponseCode::OK)
- {
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- if (!Response.IsNull())
- {
- OutResult.Response = std::move(Response);
- CHECK(OutResult.Result.Parse(OutResult.Response));
- OutResult.Success = true;
- }
- }
- else
- {
- ZEN_DEBUG("GetCacheRecords with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage(""));
- }
-
- return OutResult;
- };
-
- // Populate with some simple data
-
- CachePolicy Policy = CachePolicy::Default;
-
- const size_t ThreadCount = 128;
- const size_t KeyMultiplier = 16384;
- const size_t RecordsPerRequest = 64;
- WorkerThreadPool Pool(ThreadCount);
-
- std::atomic_size_t Completed = 0;
-
- auto Keys = new std::vector<CacheKey>[ThreadCount * KeyMultiplier];
- RwLock KeysLock;
-
- for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++)
- {
- size_t Iteration = I;
- Pool.ScheduleWork(
- [&] {
- std::vector<CacheKey> NewKeys =
- PutCacheRecords(LocalUri, "ue4.ddc"sv, "mastodon"sv, RecordsPerRequest, I * RecordsPerRequest);
- if (NewKeys.size() != RecordsPerRequest)
- {
- ZEN_DEBUG("PutCacheRecords iteration {} failed", Iteration);
- Completed.fetch_add(1);
- return;
- }
- {
- RwLock::ExclusiveLockScope _(KeysLock);
- Keys[Iteration].swap(NewKeys);
- }
- Completed.fetch_add(1);
- },
- WorkerThreadPool::EMode::EnableBacklog);
- }
- bool UseUpstream1 = false;
- while (Completed < ThreadCount * KeyMultiplier)
- {
- Sleep(8000);
-
- if (UseUpstream1)
- {
- if (Upstream2Running)
- {
- Upstream2Server.EnableTermination();
- Upstream2Server.Shutdown();
- Sleep(100);
- Upstream2Running = false;
- }
- if (!Upstream1Running)
- {
- SpawnServer(Upstream1Server, Upstream1Cfg);
- Upstream1Running = true;
- }
- UseUpstream1 = !UseUpstream1;
- }
- else
- {
- if (Upstream1Running)
- {
- Upstream1Server.EnableTermination();
- Upstream1Server.Shutdown();
- Sleep(100);
- Upstream1Running = false;
- }
- if (!Upstream2Running)
- {
- SpawnServer(Upstream2Server, Upstream2Cfg);
- Upstream2Running = true;
- }
- UseUpstream1 = !UseUpstream1;
- }
- }
-
- Completed = 0;
- for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++)
- {
- size_t Iteration = I;
- std::vector<CacheKey>& LocalKeys = Keys[Iteration];
- if (LocalKeys.empty())
- {
- Completed.fetch_add(1);
- continue;
- }
- Pool.ScheduleWork(
- [&] {
- GetCacheRecordResult Result = GetCacheRecords(LocalUri, "ue4.ddc"sv, LocalKeys, Policy);
-
- if (!Result.Success)
- {
- ZEN_DEBUG("GetCacheRecords iteration {} failed", Iteration);
- Completed.fetch_add(1);
- return;
- }
-
- if (Result.Result.Results.size() != LocalKeys.size())
- {
- ZEN_DEBUG("GetCacheRecords iteration {} empty records", Iteration);
- Completed.fetch_add(1);
- return;
- }
- for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results)
- {
- const CacheKey& ExpectedKey = LocalKeys[Index++];
- if (!Record)
- {
- continue;
- }
- if (Record->Key != ExpectedKey)
- {
- continue;
- }
- if (Record->Values.size() != 1)
- {
- continue;
- }
-
- for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values)
- {
- if (!Value.Body)
- {
- continue;
- }
- }
- }
- Completed.fetch_add(1);
- },
- WorkerThreadPool::EMode::EnableBacklog);
- }
- while (Completed < ThreadCount * KeyMultiplier)
- {
- Sleep(10);
- }
-}
-
-TEST_CASE("zcache.rpc.partialchunks")
-{
- using namespace std::literals;
- using namespace utils;
-
- ZenConfig LocalCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance Server(TestEnv);
- SpawnServer(Server, LocalCfg);
-
- std::vector<CompressedBuffer> Attachments;
-
- const auto BaseUri = fmt::format("http://localhost:{}/z$", Server.GetBasePort());
-
- auto GenerateKey = [](std::string_view Bucket, size_t KeyIndex) -> CacheKey {
- IoHash KeyHash;
- ((size_t*)(KeyHash.Hash))[0] = KeyIndex;
- return CacheKey::Create(Bucket, KeyHash);
- };
-
- auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request,
- const CacheKey& CacheKey,
- size_t AttachmentCount,
- size_t AttachmentsSize,
- CachePolicy RecordPolicy) -> std::vector<std::pair<Oid, CompressedBuffer>> {
- std::vector<std::pair<Oid, CompressedBuffer>> AttachmentBuffers;
- std::vector<cacherequests::PutCacheRecordRequestValue> Attachments;
- for (size_t AttachmentIndex = 0; AttachmentIndex < AttachmentCount; AttachmentIndex++)
- {
- CompressedBuffer Value = CreateSemiRandomBlob(AttachmentsSize);
- AttachmentBuffers.push_back(std::make_pair(Oid::NewOid(), Value));
- Attachments.push_back({.Id = AttachmentBuffers.back().first, .Body = std::move(Value)});
- }
- Request.Requests.push_back({.Key = CacheKey, .Values = Attachments, .Policy = RecordPolicy});
- return AttachmentBuffers;
- };
-
- auto PutCacheRecords = [&AppendCacheRecord, &GenerateKey](
- std::string_view BaseUri,
- std::string_view Namespace,
- std::string_view Bucket,
- size_t KeyOffset,
- size_t Num,
- size_t AttachmentCount,
- size_t AttachmentsSize =
- 8192) -> std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> {
- std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> Keys;
-
- cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)};
- for (size_t Key = 1; Key <= Num; ++Key)
- {
- const CacheKey NewCacheKey = GenerateKey(Bucket, KeyOffset + Key);
- std::vector<std::pair<Oid, CompressedBuffer>> Attachments =
- AppendCacheRecord(Request, NewCacheKey, AttachmentCount, AttachmentsSize, CachePolicy::Default);
- Keys.push_back(std::make_pair(NewCacheKey, std::move(Attachments)));
- }
-
- CbPackage Package;
- CHECK(Request.Format(Package));
-
- HttpClient Http{BaseUri};
-
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
-
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- if (Result.StatusCode != HttpResponseCode::OK)
- {
- ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage(""));
- Keys.clear();
- }
-
- return Keys;
- };
-
- std::string_view TestBucket = "partialcachevaluetests"sv;
- std::string_view TestNamespace = "ue4.ddc"sv;
- auto RecordsWithSmallAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 0, 3, 2, 4096u);
- CHECK(RecordsWithSmallAttachments.size() == 3);
- auto RecordsWithLargeAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 10, 1, 2, 8u * 1024u * 1024u);
- CHECK(RecordsWithLargeAttachments.size() == 1);
-
- struct PartialOptions
- {
- uint64_t Offset = 0ull;
- uint64_t Size = ~0ull;
- RpcAcceptOptions AcceptOptions = RpcAcceptOptions::kNone;
- };
-
- auto GetCacheChunk = [](std::string_view BaseUri,
- std::string_view Namespace,
- const CacheKey& Key,
- const Oid& ValueId,
- const PartialOptions& Options = {}) -> cacherequests::GetCacheChunksResult {
- cacherequests::GetCacheChunksRequest Request = {
- .AcceptMagic = kCbPkgMagic,
- .AcceptOptions = (uint16_t)Options.AcceptOptions,
- .Namespace = std::string(Namespace),
- .Requests = {{.Key = Key, .ValueId = ValueId, .RawOffset = Options.Offset, .RawSize = Options.Size}}};
- CbPackage Package;
- CHECK(Request.Format(Package));
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
-
- HttpClient Http{BaseUri};
-
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
-
- CHECK(Result.StatusCode == HttpResponseCode::OK);
-
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- bool Loaded = !Response.IsNull();
- CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load.");
- cacherequests::GetCacheChunksResult GetCacheChunksResult;
- CHECK(GetCacheChunksResult.Parse(Response));
- return GetCacheChunksResult;
- };
-
- auto GetAndVerifyChunk = [&GetCacheChunk](std::string_view BaseUri,
- std::string_view Namespace,
- const CacheKey& Key,
- const Oid& ChunkId,
- const CompressedBuffer& VerifyData,
- const PartialOptions& Options = {}) {
- cacherequests::GetCacheChunksResult Result = GetCacheChunk(BaseUri, Namespace, Key, ChunkId, Options);
- CHECK(Result.Results.size() == 1);
- bool CanGetPartial = ((uint16_t)Options.AcceptOptions & (uint16_t)RpcAcceptOptions::kAllowPartialCacheChunks);
- if (!CanGetPartial)
- {
- CHECK(Result.Results[0].FragmentOffset == 0);
- CHECK(Result.Results[0].Body.GetCompressedSize() == VerifyData.GetCompressedSize());
- }
- IoBuffer SourceDecompressed = VerifyData.Decompress(Options.Offset, Options.Size).AsIoBuffer();
- IoBuffer ReceivedDecompressed =
- Result.Results[0].Body.Decompress(Options.Offset - Result.Results[0].FragmentOffset, Options.Size).AsIoBuffer();
- CHECK(SourceDecompressed.GetView().EqualBytes(ReceivedDecompressed.GetView()));
- };
-
- GetAndVerifyChunk(BaseUri,
- TestNamespace,
- RecordsWithSmallAttachments[0].first,
- RecordsWithSmallAttachments[0].second[0].first,
- RecordsWithSmallAttachments[0].second[0].second);
- GetAndVerifyChunk(BaseUri,
- TestNamespace,
- RecordsWithSmallAttachments[0].first,
- RecordsWithSmallAttachments[0].second[0].first,
- RecordsWithSmallAttachments[0].second[0].second,
- PartialOptions{.Offset = 378, .Size = 519, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences});
- GetAndVerifyChunk(
- BaseUri,
- TestNamespace,
- RecordsWithSmallAttachments[0].first,
- RecordsWithSmallAttachments[0].second[0].first,
- RecordsWithSmallAttachments[0].second[0].second,
- PartialOptions{.Offset = 378,
- .Size = 519,
- .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks});
- GetAndVerifyChunk(BaseUri,
- TestNamespace,
- RecordsWithLargeAttachments[0].first,
- RecordsWithLargeAttachments[0].second[0].first,
- RecordsWithLargeAttachments[0].second[0].second,
- PartialOptions{.AcceptOptions = RpcAcceptOptions::kAllowLocalReferences});
- GetAndVerifyChunk(BaseUri,
- TestNamespace,
- RecordsWithLargeAttachments[0].first,
- RecordsWithLargeAttachments[0].second[0].first,
- RecordsWithLargeAttachments[0].second[0].second,
- PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u});
- GetAndVerifyChunk(
- BaseUri,
- TestNamespace,
- RecordsWithLargeAttachments[0].first,
- RecordsWithLargeAttachments[0].second[0].first,
- RecordsWithLargeAttachments[0].second[0].second,
- PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences});
- GetAndVerifyChunk(
- BaseUri,
- TestNamespace,
- RecordsWithLargeAttachments[0].first,
- RecordsWithLargeAttachments[0].second[0].first,
- RecordsWithLargeAttachments[0].second[0].second,
- PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowPartialCacheChunks});
- GetAndVerifyChunk(
- BaseUri,
- TestNamespace,
- RecordsWithLargeAttachments[0].first,
- RecordsWithLargeAttachments[0].second[0].first,
- RecordsWithLargeAttachments[0].second[0].second,
- PartialOptions{.Offset = 1024u * 1024u,
- .Size = 512u * 1024u,
- .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks});
- GetAndVerifyChunk(
- BaseUri,
- TestNamespace,
- RecordsWithLargeAttachments[0].first,
- RecordsWithLargeAttachments[0].second[0].first,
- RecordsWithLargeAttachments[0].second[0].second,
- PartialOptions{.Offset = 1024u * 1024u,
- .Size = 512u * 1024u,
- .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences |
- RpcAcceptOptions::kAllowPartialCacheChunks});
-}
-
-IoBuffer
-FormatPackageBody(const CbPackage& Package)
-{
- IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer();
- Body.SetContentType(HttpContentType::kCbPackage);
- return Body;
-}
-
-TEST_CASE("zcache.rpc.allpolicies")
-{
- using namespace std::literals;
- using namespace utils;
-
- ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber());
- ZenServerInstance UpstreamServer(TestEnv);
- SpawnServer(UpstreamServer, UpstreamCfg);
-
- ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port);
- ZenServerInstance LocalServer(TestEnv);
- SpawnServer(LocalServer, LocalCfg);
-
- const auto BaseUri = fmt::format("http://localhost:{}/z$", LocalServer.GetBasePort());
- HttpClient Http{BaseUri};
-
- std::string_view TestVersion = "F72150A02AE34B57A9EC91D36BA1CE08"sv;
- std::string_view TestBucket = "allpoliciestest"sv;
- std::string_view TestNamespace = "ue4.ddc"sv;
-
- // NumKeys = (2 Value vs Record)*(2 SkipData vs Default)*(2 ForceMiss vs Not)*(2 use local)
- // *(2 use remote)*(2 UseValue Policy vs not)*(4 cases per type)
- constexpr int NumKeys = 256;
- constexpr int NumValues = 4;
- Oid ValueIds[NumValues];
- IoHash Hash;
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- ExtendableStringBuilder<16> ValueName;
- ValueName << "ValueId_"sv << ValueIndex;
- static_assert(sizeof(IoHash) >= sizeof(Oid));
- ValueIds[ValueIndex] = Oid::FromMemory(IoHash::HashBuffer(ValueName.Data(), ValueName.Size() * sizeof(ValueName.Data()[0])).Hash);
- }
-
- struct KeyData;
- struct UserData
- {
- UserData& Set(KeyData* InKeyData, int InValueIndex)
- {
- Data = InKeyData;
- ValueIndex = InValueIndex;
- return *this;
- }
- KeyData* Data = nullptr;
- int ValueIndex = 0;
- };
- struct KeyData
- {
- CompressedBuffer BufferValues[NumValues];
- uint64_t IntValues[NumValues];
- UserData ValueUserData[NumValues];
- bool ReceivedChunk[NumValues];
- CacheKey Key;
- UserData KeyUserData;
- uint32_t KeyIndex = 0;
- bool GetRequestsData = true;
- bool UseValueAPI = false;
- bool UseValuePolicy = false;
- bool ForceMiss = false;
- bool UseLocal = true;
- bool UseRemote = true;
- bool ShouldBeHit = true;
- bool ReceivedPut = false;
- bool ReceivedGet = false;
- bool ReceivedPutValue = false;
- bool ReceivedGetValue = false;
- };
- struct CachePutRequest
- {
- CacheKey Key;
- CbObject Record;
- CacheRecordPolicy Policy;
- KeyData* Values;
- UserData* Data;
- };
- struct CachePutValueRequest
- {
- CacheKey Key;
- CompressedBuffer Value;
- CachePolicy Policy;
- UserData* Data;
- };
- struct CacheGetRequest
- {
- CacheKey Key;
- CacheRecordPolicy Policy;
- UserData* Data;
- };
- struct CacheGetValueRequest
- {
- CacheKey Key;
- CachePolicy Policy;
- UserData* Data;
- };
- struct CacheGetChunkRequest
- {
- CacheKey Key;
- Oid ValueId;
- uint64_t RawOffset;
- uint64_t RawSize;
- IoHash RawHash;
- CachePolicy Policy;
- UserData* Data;
- };
-
- KeyData KeyDatas[NumKeys];
- std::vector<CachePutRequest> PutRequests;
- std::vector<CachePutValueRequest> PutValueRequests;
- std::vector<CacheGetRequest> GetRequests;
- std::vector<CacheGetValueRequest> GetValueRequests;
- std::vector<CacheGetChunkRequest> ChunkRequests;
-
- for (uint32_t KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex)
- {
- IoHashStream KeyWriter;
- KeyWriter.Append(TestVersion.data(), TestVersion.length() * sizeof(TestVersion.data()[0]));
- KeyWriter.Append(&KeyIndex, sizeof(KeyIndex));
- IoHash KeyHash = KeyWriter.GetHash();
- KeyData& KeyData = KeyDatas[KeyIndex];
-
- KeyData.Key = CacheKey::Create(TestBucket, KeyHash);
- KeyData.KeyIndex = KeyIndex;
- KeyData.GetRequestsData = (KeyIndex & (1 << 1)) == 0;
- KeyData.UseValueAPI = (KeyIndex & (1 << 2)) != 0;
- KeyData.UseValuePolicy = (KeyIndex & (1 << 3)) != 0;
- KeyData.ForceMiss = (KeyIndex & (1 << 4)) == 0;
- KeyData.UseLocal = (KeyIndex & (1 << 5)) == 0;
- KeyData.UseRemote = (KeyIndex & (1 << 6)) == 0;
- KeyData.ShouldBeHit = !KeyData.ForceMiss && (KeyData.UseLocal || KeyData.UseRemote);
- CachePolicy SharedPolicy = KeyData.UseLocal ? CachePolicy::Local : CachePolicy::None;
- SharedPolicy |= KeyData.UseRemote ? CachePolicy::Remote : CachePolicy::None;
- CachePolicy PutPolicy = SharedPolicy;
- CachePolicy GetPolicy = SharedPolicy;
- GetPolicy |= !KeyData.GetRequestsData ? CachePolicy::SkipData : CachePolicy::None;
- CacheKey& Key = KeyData.Key;
-
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- KeyData.IntValues[ValueIndex] = static_cast<uint64_t>(KeyIndex) | (static_cast<uint64_t>(ValueIndex) << 32);
- KeyData.BufferValues[ValueIndex] =
- CompressedBuffer::Compress(SharedBuffer::MakeView(&KeyData.IntValues[ValueIndex], sizeof(KeyData.IntValues[ValueIndex])));
- KeyData.ReceivedChunk[ValueIndex] = false;
- }
-
- UserData& KeyUserData = KeyData.KeyUserData.Set(&KeyData, -1);
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- KeyData.ValueUserData[ValueIndex].Set(&KeyData, ValueIndex);
- }
- if (!KeyData.UseValueAPI)
- {
- CbObjectWriter Builder;
- Builder.BeginObject("key"sv);
- Builder << "Bucket"sv << Key.Bucket << "Hash"sv << Key.Hash;
- Builder.EndObject();
- Builder.BeginArray("Values"sv);
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- Builder.BeginObject();
- Builder.AddObjectId("Id"sv, ValueIds[ValueIndex]);
- Builder.AddBinaryAttachment("RawHash"sv, KeyData.BufferValues[ValueIndex].DecodeRawHash());
- Builder.AddInteger("RawSize"sv, KeyData.BufferValues[ValueIndex].DecodeRawSize());
- Builder.EndObject();
- }
- Builder.EndArray();
-
- CacheRecordPolicy PutRecordPolicy;
- CacheRecordPolicy GetRecordPolicy;
- if (!KeyData.UseValuePolicy)
- {
- PutRecordPolicy = CacheRecordPolicy(PutPolicy);
- GetRecordPolicy = CacheRecordPolicy(GetPolicy);
- }
- else
- {
- // Switch the SkipData field in the Record policy so that if the CacheStore ignores the ValuePolicies
- // it will use the wrong value for SkipData and fail our tests.
- CacheRecordPolicyBuilder PutBuilder(PutPolicy ^ CachePolicy::SkipData);
- CacheRecordPolicyBuilder GetBuilder(GetPolicy ^ CachePolicy::SkipData);
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- PutBuilder.AddValuePolicy(ValueIds[ValueIndex], PutPolicy);
- GetBuilder.AddValuePolicy(ValueIds[ValueIndex], GetPolicy);
- }
- PutRecordPolicy = PutBuilder.Build();
- GetRecordPolicy = GetBuilder.Build();
- }
- if (!KeyData.ForceMiss)
- {
- PutRequests.push_back({Key, Builder.Save(), PutRecordPolicy, &KeyData, &KeyUserData});
- }
- GetRequests.push_back({Key, GetRecordPolicy, &KeyUserData});
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- UserData& ValueUserData = KeyData.ValueUserData[ValueIndex];
- ChunkRequests.push_back({Key, ValueIds[ValueIndex], 0, UINT64_MAX, IoHash(), GetPolicy, &ValueUserData});
- }
- }
- else
- {
- if (!KeyData.ForceMiss)
- {
- PutValueRequests.push_back({Key, KeyData.BufferValues[0], PutPolicy, &KeyUserData});
- }
- GetValueRequests.push_back({Key, GetPolicy, &KeyUserData});
- ChunkRequests.push_back({Key, Oid::Zero, 0, UINT64_MAX, IoHash(), GetPolicy, &KeyUserData});
- }
- }
-
- // PutCacheRecords
- {
- CachePolicy BatchDefaultPolicy = CachePolicy::Default;
- cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
- .DefaultPolicy = BatchDefaultPolicy,
- .Namespace = std::string(TestNamespace)};
- Request.Requests.reserve(PutRequests.size());
- for (CachePutRequest& PutRequest : PutRequests)
- {
- cacherequests::PutCacheRecordRequest& RecordRequest = Request.Requests.emplace_back();
- RecordRequest.Key = PutRequest.Key;
- RecordRequest.Policy = PutRequest.Policy;
- RecordRequest.Values.reserve(NumValues);
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- RecordRequest.Values.push_back({.Id = ValueIds[ValueIndex], .Body = PutRequest.Values->BufferValues[ValueIndex]});
- }
- PutRequest.Data->Data->ReceivedPut = true;
- }
-
- CbPackage Package;
- CHECK(Request.Format(Package));
- IoBuffer Body = FormatPackageBody(Package);
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
- CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "PutCacheRecords unexpectedly failed.");
- }
-
- // PutCacheValues
- {
- CachePolicy BatchDefaultPolicy = CachePolicy::Default;
-
- cacherequests::PutCacheValuesRequest Request = {.AcceptMagic = kCbPkgMagic,
- .DefaultPolicy = BatchDefaultPolicy,
- .Namespace = std::string(TestNamespace)};
- Request.Requests.reserve(PutValueRequests.size());
- for (CachePutValueRequest& PutRequest : PutValueRequests)
- {
- Request.Requests.push_back({.Key = PutRequest.Key, .Body = PutRequest.Value, .Policy = PutRequest.Policy});
- PutRequest.Data->Data->ReceivedPutValue = true;
- }
-
- CbPackage Package;
- CHECK(Request.Format(Package));
-
- IoBuffer Body = FormatPackageBody(Package);
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
- CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "PutCacheValues unexpectedly failed.");
- }
-
- for (KeyData& KeyData : KeyDatas)
- {
- if (!KeyData.ForceMiss)
- {
- if (!KeyData.UseValueAPI)
- {
- CHECK_MESSAGE(KeyData.ReceivedPut, WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put.").c_str());
- }
- else
- {
- CHECK_MESSAGE(KeyData.ReceivedPutValue,
- WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put to ValueAPI.").c_str());
- }
- }
- }
-
- // GetCacheRecords
- {
- CachePolicy BatchDefaultPolicy = CachePolicy::Default;
- cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic,
- .DefaultPolicy = BatchDefaultPolicy,
- .Namespace = std::string(TestNamespace)};
- Request.Requests.reserve(GetRequests.size());
- for (CacheGetRequest& GetRequest : GetRequests)
- {
- Request.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy});
- }
-
- CbPackage Package;
- CHECK(Request.Format(Package));
- IoBuffer Body = FormatPackageBody(Package);
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
- CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheRecords unexpectedly failed.");
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- bool Loaded = !Response.IsNull();
- CHECK_MESSAGE(Loaded, "GetCacheRecords response failed to load.");
- cacherequests::GetCacheRecordsResult RequestResult;
- CHECK(RequestResult.Parse(Response));
- CHECK_MESSAGE(RequestResult.Results.size() == GetRequests.size(), "GetCacheRecords response count did not match request count.");
- for (int Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& RecordResult : RequestResult.Results)
- {
- bool Succeeded = RecordResult.has_value();
- CacheGetRequest& GetRequest = GetRequests[Index++];
- KeyData* KeyData = GetRequest.Data->Data;
- KeyData->ReceivedGet = true;
- WriteToString<32> Name("Get(", KeyData->KeyIndex, ")");
- if (KeyData->ShouldBeHit)
- {
- CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str());
- }
- else if (KeyData->ForceMiss)
- {
- CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, " unexpectedly succeeded.").c_str());
- }
- if (!KeyData->ForceMiss && Succeeded)
- {
- CHECK_MESSAGE(RecordResult->Values.size() == NumValues,
- WriteToString<32>(Name, " number of values did not match.").c_str());
- for (const cacherequests::GetCacheRecordResultValue& Value : RecordResult->Values)
- {
- int ExpectedValueIndex = 0;
- for (; ExpectedValueIndex < NumValues; ++ExpectedValueIndex)
- {
- if (ValueIds[ExpectedValueIndex] == Value.Id)
- {
- break;
- }
- }
- CHECK_MESSAGE(ExpectedValueIndex < NumValues, WriteToString<32>(Name, " could not find matching ValueId.").c_str());
-
- WriteToString<32> ValueName("Get(", KeyData->KeyIndex, ",", ExpectedValueIndex, ")");
-
- CompressedBuffer ExpectedValue = KeyData->BufferValues[ExpectedValueIndex];
- CHECK_MESSAGE(Value.RawHash == ExpectedValue.DecodeRawHash(),
- WriteToString<32>(ValueName, " RawHash did not match.").c_str());
- CHECK_MESSAGE(Value.RawSize == ExpectedValue.DecodeRawSize(),
- WriteToString<32>(ValueName, " RawSize did not match.").c_str());
-
- if (KeyData->GetRequestsData)
- {
- SharedBuffer Buffer = Value.Body.Decompress();
- CHECK_MESSAGE(Buffer.GetSize() == Value.RawSize,
- WriteToString<32>(ValueName, " BufferSize did not match RawSize.").c_str());
- uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0];
- uint64_t ExpectedIntValue = KeyData->IntValues[ExpectedValueIndex];
- CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(ValueName, " had unexpected data.").c_str());
- }
- }
- }
- }
- }
-
- // GetCacheValues
- {
- CachePolicy BatchDefaultPolicy = CachePolicy::Default;
-
- cacherequests::GetCacheValuesRequest GetCacheValuesRequest = {.AcceptMagic = kCbPkgMagic,
- .DefaultPolicy = BatchDefaultPolicy,
- .Namespace = std::string(TestNamespace)};
- GetCacheValuesRequest.Requests.reserve(GetValueRequests.size());
- for (CacheGetValueRequest& GetRequest : GetValueRequests)
- {
- GetCacheValuesRequest.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy});
- }
-
- CbPackage Package;
- CHECK(GetCacheValuesRequest.Format(Package));
-
- IoBuffer Body = FormatPackageBody(Package);
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
- CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheValues unexpectedly failed.");
- IoBuffer MessageBuffer(Result.ResponsePayload);
- CbPackage Response = ParsePackageMessage(MessageBuffer);
- bool Loaded = !Response.IsNull();
- CHECK_MESSAGE(Loaded, "GetCacheValues response failed to load.");
- cacherequests::GetCacheValuesResult GetCacheValuesResult;
- CHECK(GetCacheValuesResult.Parse(Response));
- for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheValuesResult.Results)
- {
- bool Succeeded = ValueResult.RawHash != IoHash::Zero;
- CacheGetValueRequest& Request = GetValueRequests[Index++];
- KeyData* KeyData = Request.Data->Data;
- KeyData->ReceivedGetValue = true;
- WriteToString<32> Name("GetValue("sv, KeyData->KeyIndex, ")"sv);
-
- if (KeyData->ShouldBeHit)
- {
- CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str());
- }
- else if (KeyData->ForceMiss)
- {
- CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, "unexpectedly succeeded.").c_str());
- }
- if (!KeyData->ForceMiss && Succeeded)
- {
- CompressedBuffer ExpectedValue = KeyData->BufferValues[0];
- CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(),
- WriteToString<32>(Name, " RawHash did not match.").c_str());
- CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(),
- WriteToString<32>(Name, " RawSize did not match.").c_str());
-
- if (KeyData->GetRequestsData)
- {
- SharedBuffer Buffer = ValueResult.Body.Decompress();
- CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize,
- WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str());
- uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0];
- uint64_t ExpectedIntValue = KeyData->IntValues[0];
- CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str());
- }
- }
- }
- }
-
- // GetCacheChunks
- {
- std::sort(ChunkRequests.begin(), ChunkRequests.end(), [](CacheGetChunkRequest& A, CacheGetChunkRequest& B) {
- return A.Key.Hash < B.Key.Hash;
- });
- CachePolicy BatchDefaultPolicy = CachePolicy::Default;
- cacherequests::GetCacheChunksRequest GetCacheChunksRequest = {.AcceptMagic = kCbPkgMagic,
- .DefaultPolicy = BatchDefaultPolicy,
- .Namespace = std::string(TestNamespace)};
- GetCacheChunksRequest.Requests.reserve(ChunkRequests.size());
- for (CacheGetChunkRequest& ChunkRequest : ChunkRequests)
- {
- GetCacheChunksRequest.Requests.push_back({.Key = ChunkRequest.Key,
- .ValueId = ChunkRequest.ValueId,
- .ChunkId = IoHash(),
- .RawOffset = ChunkRequest.RawOffset,
- .RawSize = ChunkRequest.RawSize,
- .Policy = ChunkRequest.Policy});
- }
- CbPackage Package;
- CHECK(GetCacheChunksRequest.Format(Package));
-
- IoBuffer Body = FormatPackageBody(Package);
- HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}});
- CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheChunks unexpectedly failed.");
- CbPackage Response = ParsePackageMessage(Result.ResponsePayload);
- bool Loaded = !Response.IsNull();
- CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load.");
- cacherequests::GetCacheChunksResult GetCacheChunksResult;
- CHECK(GetCacheChunksResult.Parse(Response));
- CHECK_MESSAGE(GetCacheChunksResult.Results.size() == ChunkRequests.size(),
- "GetCacheChunks response count did not match request count.");
-
- for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheChunksResult.Results)
- {
- bool Succeeded = ValueResult.RawHash != IoHash::Zero;
-
- CacheGetChunkRequest& Request = ChunkRequests[Index++];
- KeyData* KeyData = Request.Data->Data;
- int ValueIndex = Request.Data->ValueIndex >= 0 ? Request.Data->ValueIndex : 0;
- KeyData->ReceivedChunk[ValueIndex] = true;
- WriteToString<32> Name("GetChunks("sv, KeyData->KeyIndex, ","sv, ValueIndex, ")"sv);
-
- if (KeyData->ShouldBeHit)
- {
- CHECK_MESSAGE(Succeeded, WriteToString<256>(Name, " unexpectedly failed."sv).c_str());
- }
- else if (KeyData->ForceMiss)
- {
- CHECK_MESSAGE(!Succeeded, WriteToString<256>(Name, " unexpectedly succeeded."sv).c_str());
- }
- if (KeyData->ShouldBeHit && Succeeded)
- {
- CompressedBuffer ExpectedValue = KeyData->BufferValues[ValueIndex];
- CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(),
- WriteToString<32>(Name, " had unexpected RawHash.").c_str());
- CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(),
- WriteToString<32>(Name, " had unexpected RawSize.").c_str());
-
- if (KeyData->GetRequestsData)
- {
- SharedBuffer Buffer = ValueResult.Body.Decompress();
- CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize,
- WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str());
- uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0];
- uint64_t ExpectedIntValue = KeyData->IntValues[ValueIndex];
- CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str());
- }
- }
- }
- }
-
- for (KeyData& KeyData : KeyDatas)
- {
- if (!KeyData.UseValueAPI)
- {
- CHECK_MESSAGE(KeyData.ReceivedGet, WriteToString<32>("Get(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str());
- for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex)
- {
- CHECK_MESSAGE(
- KeyData.ReceivedChunk[ValueIndex],
- WriteToString<32>("GetChunks(", KeyData.KeyIndex, ",", ValueIndex, ") was unexpectedly not received.").c_str());
- }
- }
- else
- {
- CHECK_MESSAGE(KeyData.ReceivedGetValue,
- WriteToString<32>("GetValue(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str());
- CHECK_MESSAGE(KeyData.ReceivedChunk[0],
- WriteToString<32>("GetChunks(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str());
- }
- }
-}
-
-class ZenServerTestHelper
-{
-public:
- ZenServerTestHelper(std::string_view HelperId, int ServerCount) : m_HelperId{HelperId}, m_ServerCount{ServerCount} {}
- ~ZenServerTestHelper() {}
-
- void SpawnServers(std::string_view AdditionalServerArgs = std::string_view())
- {
- SpawnServers([](ZenServerInstance&) {}, AdditionalServerArgs);
- }
-
- void SpawnServers(auto&& Callback, std::string_view AdditionalServerArgs)
- {
- ZEN_INFO("{}: spawning {} server instances", m_HelperId, m_ServerCount);
-
- m_Instances.resize(m_ServerCount);
-
- for (int i = 0; i < m_ServerCount; ++i)
- {
- auto& Instance = m_Instances[i];
- Instance = std::make_unique<ZenServerInstance>(TestEnv);
- Instance->SetTestDir(TestEnv.CreateNewTestDir());
- }
-
- for (int i = 0; i < m_ServerCount; ++i)
- {
- auto& Instance = m_Instances[i];
- Callback(*Instance);
- }
-
- for (int i = 0; i < m_ServerCount; ++i)
- {
- auto& Instance = m_Instances[i];
- Instance->SpawnServer(TestEnv.GetNewPortNumber(), AdditionalServerArgs);
- }
-
- for (int i = 0; i < m_ServerCount; ++i)
- {
- auto& Instance = m_Instances[i];
- uint16_t PortNumber = Instance->WaitUntilReady();
- CHECK_MESSAGE(PortNumber != 0, Instance->GetLogOutput());
- }
- }
-
- ZenServerInstance& GetInstance(int Index) { return *m_Instances[Index]; }
-
-private:
- std::string m_HelperId;
- int m_ServerCount = 0;
- std::vector<std::unique_ptr<ZenServerInstance>> m_Instances;
-};
-
TEST_CASE("http.basics")
{
using namespace std::literals;
@@ -3107,1787 +320,6 @@ TEST_CASE("http.package")
CHECK_EQ(ResponsePackage, TestPackage);
}
-std::string
-OidAsString(const Oid& Id)
-{
- StringBuilder<25> OidStringBuilder;
- Id.ToString(OidStringBuilder);
- return OidStringBuilder.ToString();
-}
-
-CbPackage
-CreateOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments)
-{
- CbPackage Package;
- CbObjectWriter Object;
- Object << "key"sv << OidAsString(Id);
- if (!Attachments.empty())
- {
- Object.BeginArray("bulkdata");
- for (const auto& Attachment : Attachments)
- {
- CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash());
- Object.BeginObject();
- Object << "id"sv << Attachment.first;
- Object << "type"sv
- << "Standard"sv;
- Object << "data"sv << Attach;
- Object.EndObject();
-
- Package.AddAttachment(Attach);
- ZEN_DEBUG("Added attachment {}", Attach.GetHash());
- }
- Object.EndArray();
- }
- Package.SetObject(Object.Save());
- return Package;
-};
-
-CbObject
-CreateOplogOp(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments)
-{
- CbObjectWriter Object;
- Object << "key"sv << OidAsString(Id);
- if (!Attachments.empty())
- {
- Object.BeginArray("bulkdata");
- for (const auto& Attachment : Attachments)
- {
- CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash());
- Object.BeginObject();
- Object << "id"sv << Attachment.first;
- Object << "type"sv
- << "Standard"sv;
- Object << "data"sv << Attach;
- Object.EndObject();
-
- ZEN_DEBUG("Added attachment {}", Attach.GetHash());
- }
- Object.EndArray();
- }
- return Object.Save();
-};
-
-enum CbWriterMeta
-{
- BeginObject,
- EndObject,
- BeginArray,
- EndArray
-};
-
-inline CbWriter&
-operator<<(CbWriter& Writer, CbWriterMeta Meta)
-{
- switch (Meta)
- {
- case BeginObject:
- Writer.BeginObject();
- break;
- case EndObject:
- Writer.EndObject();
- break;
- case BeginArray:
- Writer.BeginArray();
- break;
- case EndArray:
- Writer.EndArray();
- break;
- default:
- ZEN_ASSERT(false);
- }
- return Writer;
-}
-
-TEST_CASE("project.remote")
-{
- using namespace std::literals;
- using namespace utils;
-
- ZenServerTestHelper Servers("remote", 3);
- Servers.SpawnServers("--debug");
-
- std::vector<Oid> OpIds;
- const size_t OpCount = 24;
- OpIds.reserve(OpCount);
- for (size_t I = 0; I < OpCount; ++I)
- {
- OpIds.emplace_back(Oid::NewOid());
- }
-
- std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments;
- {
- std::vector<std::size_t> AttachmentSizes(
- {7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 33466, 1093, 4269, 2257, 3685, 13489, 97194,
- 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 224024, 51582, 5251, 491, 2u * 1024u * 1024u + 124u,
- 74607, 18135, 3767, 154045, 4415, 5007, 8876, 96761, 3359, 8526, 4097, 4855, 48225});
- auto It = AttachmentSizes.begin();
- Attachments[OpIds[0]] = {};
- Attachments[OpIds[1]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[2]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[3]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[4]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
- Attachments[OpIds[5]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[6]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[7]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[8]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
- Attachments[OpIds[9]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[10]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[11]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
- Attachments[OpIds[12]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[13]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[14]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[15]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[16]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
- Attachments[OpIds[17]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[18]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[19]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
- Attachments[OpIds[20]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[21]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[22]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
- Attachments[OpIds[23]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- ZEN_ASSERT(It == AttachmentSizes.end());
- }
-
- // Note: This is a clone of the function in projectstore.cpp
- auto ComputeOpKey = [](const CbObjectView& Op) -> Oid {
- using namespace std::literals;
-
- XXH3_128Stream_deprecated KeyHasher;
- Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); });
- XXH3_128 KeyHash128 = KeyHasher.GetHash();
-
- Oid KeyHash;
- memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash);
-
- return KeyHash;
- };
-
- auto AddOp = [ComputeOpKey](const CbObject& Op, std::unordered_map<Oid, uint32_t, Oid::Hasher>& Ops) {
- const Oid Id = ComputeOpKey(Op);
- IoBuffer Buffer = Op.GetBuffer().AsIoBuffer();
- const uint32_t OpCoreHash = uint32_t(XXH3_64bits(Buffer.GetData(), Buffer.GetSize()) & 0xffffFFFF);
- Ops.insert({Id, OpCoreHash});
- };
-
- auto MakeProject = [](std::string_view UrlBase, std::string_view ProjectName) {
- CbObjectWriter Project;
- Project.AddString("id"sv, ProjectName);
- Project.AddString("root"sv, ""sv);
- Project.AddString("engine"sv, ""sv);
- Project.AddString("project"sv, ""sv);
- Project.AddString("projectfile"sv, ""sv);
- IoBuffer ProjectPayload = Project.Save().GetBuffer().AsIoBuffer();
- ProjectPayload.SetContentType(HttpContentType::kCbObject);
-
- HttpClient Http{UrlBase};
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}", ProjectName), ProjectPayload);
- CHECK(Response);
- };
-
- auto MakeOplog = [](std::string_view UrlBase, std::string_view ProjectName, std::string_view OplogName) {
- HttpClient Http{UrlBase};
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName), IoBuffer{});
- CHECK(Response);
- };
-
- auto MakeOp = [](std::string_view UrlBase, std::string_view ProjectName, std::string_view OplogName, const CbPackage& OpPackage) {
- zen::BinaryWriter MemOut;
- legacy::SaveCbPackage(OpPackage, MemOut);
- IoBuffer Body{IoBuffer::Wrap, MemOut.GetData(), MemOut.GetSize()};
- Body.SetContentType(HttpContentType::kCbPackage);
-
- HttpClient Http{UrlBase};
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/new", ProjectName, OplogName), Body);
- CHECK(Response);
- };
-
- MakeProject(Servers.GetInstance(0).GetBaseUri(), "proj0");
- MakeOplog(Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0");
-
- std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps;
- for (const Oid& OpId : OpIds)
- {
- CbPackage OpPackage = CreateOplogPackage(OpId, Attachments[OpId]);
- CHECK(OpPackage.GetAttachments().size() == Attachments[OpId].size());
- AddOp(OpPackage.GetObject(), SourceOps);
- MakeOp(Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0", OpPackage);
- }
-
- std::vector<IoHash> AttachmentHashes;
- AttachmentHashes.reserve(Attachments.size());
- for (const auto& AttachmentOplog : Attachments)
- {
- for (const auto& Attachment : AttachmentOplog.second)
- {
- AttachmentHashes.emplace_back(Attachment.second.DecodeRawHash());
- }
- }
-
- auto MakeCbObjectPayload = [](std::function<void(CbObjectWriter & Writer)> Write) -> IoBuffer {
- CbObjectWriter Writer;
- Write(Writer);
- IoBuffer Result = Writer.Save().GetBuffer().AsIoBuffer();
- Result.MakeOwned();
- Result.SetContentType(HttpContentType::kCbObject);
- return Result;
- };
-
- auto ValidateAttachments =
- [&MakeCbObjectPayload, &AttachmentHashes, &Servers](int ServerIndex, std::string_view Project, std::string_view Oplog) {
- HttpClient Http{Servers.GetInstance(ServerIndex).GetBaseUri()};
-
- IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "getchunks"sv;
- Writer << "chunks"sv << BeginArray;
- for (const IoHash& Chunk : AttachmentHashes)
- {
- Writer << Chunk;
- }
- Writer << EndArray; // chunks
- });
-
- HttpClient::Response Response =
- Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", Project, Oplog), Payload, {{"Accept", "application/x-ue-cbpkg"}});
- CHECK(Response);
- CbPackage ResponsePackage = ParsePackageMessage(Response.ResponsePayload);
- CHECK(ResponsePackage.GetAttachments().size() == AttachmentHashes.size());
- for (auto A : ResponsePackage.GetAttachments())
- {
- CHECK(IoHash::HashBuffer(A.AsCompressedBinary().DecompressToComposite()) == A.GetHash());
- }
- };
-
- auto ValidateOplog = [&SourceOps, &AddOp, &Servers](int ServerIndex, std::string_view Project, std::string_view Oplog) {
- std::unordered_map<Oid, uint32_t, Oid::Hasher> TargetOps;
- std::vector<CbObject> ResultingOplog;
-
- HttpClient Http{Servers.GetInstance(ServerIndex).GetBaseUri()};
- HttpClient::Response Response = Http.Get(fmt::format("/prj/{}/oplog/{}/entries", Project, Oplog));
- CHECK(Response);
-
- IoBuffer Payload(Response.ResponsePayload);
- CbObject OplogResonse = LoadCompactBinaryObject(Payload);
- CbArrayView EntriesArray = OplogResonse["entries"sv].AsArrayView();
-
- for (CbFieldView OpEntry : EntriesArray)
- {
- CbObjectView Core = OpEntry.AsObjectView();
- BinaryWriter Writer;
- Core.CopyTo(Writer);
- MemoryView OpView = Writer.GetView();
- IoBuffer OpBuffer(IoBuffer::Wrap, OpView.GetData(), OpView.GetSize());
- CbObject Op(SharedBuffer(OpBuffer), CbFieldType::HasFieldType);
- AddOp(Op, TargetOps);
- }
- CHECK(SourceOps == TargetOps);
- };
-
- auto HttpWaitForCompletion = [](ZenServerInstance& Server, const HttpClient::Response& Response) {
- REQUIRE(Response);
- const uint64_t JobId = ParseInt<uint64_t>(Response.AsText()).value_or(0);
- CHECK(JobId != 0);
-
- HttpClient Http{Server.GetBaseUri()};
-
- while (true)
- {
- HttpClient::Response StatusResponse =
- Http.Get(fmt::format("/admin/jobs/{}", JobId), {{"Accept", ToString(ZenContentType::kCbObject)}});
- CHECK(StatusResponse);
- CbObject ResponseObject = StatusResponse.AsObject();
- std::string_view Status = ResponseObject["Status"sv].AsString();
- CHECK(Status != "Aborted"sv);
- if (Status == "Complete"sv)
- {
- return;
- }
- Sleep(10);
- }
- };
-
- SUBCASE("File")
- {
- ScopedTemporaryDirectory TempDir;
- {
- IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "export"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "maxblocksize"sv << 3072u;
- Writer << "maxchunkembedsize"sv << 1296u;
- Writer << "chunkfilesizelimit"sv << 5u * 1024u;
- Writer << "force"sv << false;
- Writer << "file"sv << BeginObject;
- {
- Writer << "path"sv << path;
- Writer << "name"sv
- << "proj0_oplog0"sv;
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
-
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(0), Response);
- }
- {
- MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy");
- MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy");
-
- IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "import"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "force"sv << false;
- Writer << "file"sv << BeginObject;
- {
- Writer << "path"sv << path;
- Writer << "name"sv
- << "proj0_oplog0"sv;
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(1).GetBaseUri()};
-
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(1), Response);
- }
- ValidateAttachments(1, "proj0_copy", "oplog0_copy");
- ValidateOplog(1, "proj0_copy", "oplog0_copy");
- }
-
- SUBCASE("File disable blocks")
- {
- ScopedTemporaryDirectory TempDir;
- {
- IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "export"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "maxblocksize"sv << 3072u;
- Writer << "maxchunkembedsize"sv << 1296u;
- Writer << "chunkfilesizelimit"sv << 5u * 1024u;
- Writer << "force"sv << false;
- Writer << "file"sv << BeginObject;
- {
- Writer << "path"sv << TempDir.Path().string();
- Writer << "name"sv
- << "proj0_oplog0"sv;
- Writer << "disableblocks"sv << true;
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
-
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(0), Response);
- }
- {
- MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy");
- MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy");
- IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "import"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "force"sv << false;
- Writer << "file"sv << BeginObject;
- {
- Writer << "path"sv << TempDir.Path().string();
- Writer << "name"sv
- << "proj0_oplog0"sv;
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(1).GetBaseUri()};
-
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(1), Response);
- }
- ValidateAttachments(1, "proj0_copy", "oplog0_copy");
- ValidateOplog(1, "proj0_copy", "oplog0_copy");
- }
-
- SUBCASE("File force temp blocks")
- {
- ScopedTemporaryDirectory TempDir;
- {
- IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "export"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "maxblocksize"sv << 3072u;
- Writer << "maxchunkembedsize"sv << 1296u;
- Writer << "chunkfilesizelimit"sv << 5u * 1024u;
- Writer << "force"sv << false;
- Writer << "file"sv << BeginObject;
- {
- Writer << "path"sv << TempDir.Path().string();
- Writer << "name"sv
- << "proj0_oplog0"sv;
- Writer << "enabletempblocks"sv << true;
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(0), Response);
- }
- {
- MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy");
- MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy");
- IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "import"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "force"sv << false;
- Writer << "file"sv << BeginObject;
- {
- Writer << "path"sv << TempDir.Path().string();
- Writer << "name"sv
- << "proj0_oplog0"sv;
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(1).GetBaseUri()};
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(1), Response);
- }
- ValidateAttachments(1, "proj0_copy", "oplog0_copy");
- ValidateOplog(1, "proj0_copy", "oplog0_copy");
- }
-
- SUBCASE("Zen")
- {
- ScopedTemporaryDirectory TempDir;
- {
- std::string ExportSourceUri = Servers.GetInstance(0).GetBaseUri();
- std::string ExportTargetUri = Servers.GetInstance(1).GetBaseUri();
- MakeProject(ExportTargetUri, "proj0_copy");
- MakeOplog(ExportTargetUri, "proj0_copy", "oplog0_copy");
-
- IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "export"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "maxblocksize"sv << 3072u;
- Writer << "maxchunkembedsize"sv << 1296u;
- Writer << "chunkfilesizelimit"sv << 5u * 1024u;
- Writer << "force"sv << false;
- Writer << "zen"sv << BeginObject;
- {
- Writer << "url"sv << ExportTargetUri.substr(7);
- Writer << "project"
- << "proj0_copy";
- Writer << "oplog"
- << "oplog0_copy";
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(0).GetBaseUri()};
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(0), Response);
- }
- ValidateAttachments(1, "proj0_copy", "oplog0_copy");
- ValidateOplog(1, "proj0_copy", "oplog0_copy");
-
- {
- std::string ImportSourceUri = Servers.GetInstance(1).GetBaseUri();
- std::string ImportTargetUri = Servers.GetInstance(2).GetBaseUri();
- MakeProject(ImportTargetUri, "proj1");
- MakeOplog(ImportTargetUri, "proj1", "oplog1");
-
- IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) {
- Writer << "method"sv
- << "import"sv;
- Writer << "params" << BeginObject;
- {
- Writer << "force"sv << false;
- Writer << "zen"sv << BeginObject;
- {
- Writer << "url"sv << ImportSourceUri.substr(7);
- Writer << "project"
- << "proj0_copy";
- Writer << "oplog"
- << "oplog0_copy";
- }
- Writer << EndObject; // "file"
- }
- Writer << EndObject; // "params"
- });
-
- HttpClient Http{Servers.GetInstance(2).GetBaseUri()};
- HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj1", "oplog1"), Payload);
- HttpWaitForCompletion(Servers.GetInstance(2), Response);
- }
- ValidateAttachments(2, "proj1", "oplog1");
- ValidateOplog(2, "proj1", "oplog1");
- }
-}
-
-TEST_CASE("project.rpcappendop")
-{
- using namespace std::literals;
- using namespace utils;
-
- ZenServerTestHelper Servers("remote", 2);
- Servers.SpawnServers("--debug");
-
- std::vector<Oid> OpIds;
- const size_t OpCount = 24;
- OpIds.reserve(OpCount);
- for (size_t I = 0; I < OpCount; ++I)
- {
- OpIds.emplace_back(Oid::NewOid());
- }
-
- std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments;
- {
- std::vector<std::size_t> AttachmentSizes(
- {7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 33466, 1093, 4269, 2257, 3685, 13489, 97194,
- 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 224024, 51582, 5251, 491, 2u * 1024u * 1024u + 124u,
- 74607, 18135, 3767, 154045, 4415, 5007, 8876, 96761, 3359, 8526, 4097, 4855, 48225});
- auto It = AttachmentSizes.begin();
- Attachments[OpIds[0]] = {};
- Attachments[OpIds[1]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[2]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[3]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[4]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
- Attachments[OpIds[5]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[6]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[7]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[8]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
- Attachments[OpIds[9]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[10]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[11]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
- Attachments[OpIds[12]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++});
- Attachments[OpIds[13]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[14]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[15]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[16]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
- Attachments[OpIds[17]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[18]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++});
- Attachments[OpIds[19]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{});
- Attachments[OpIds[20]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[21]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- Attachments[OpIds[22]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++});
- Attachments[OpIds[23]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++});
- ZEN_ASSERT(It == AttachmentSizes.end());
- }
-
- // Note: This is a clone of the function in projectstore.cpp
- auto ComputeOpKey = [](const CbObjectView& Op) -> Oid {
- using namespace std::literals;
-
- XXH3_128Stream_deprecated KeyHasher;
- Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); });
- XXH3_128 KeyHash128 = KeyHasher.GetHash();
-
- Oid KeyHash;
- memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash);
-
- return KeyHash;
- };
-
- auto AddOp = [ComputeOpKey](const CbObject& Op, std::unordered_map<Oid, uint32_t, Oid::Hasher>& Ops) {
- const Oid Id = ComputeOpKey(Op);
- IoBuffer Buffer = Op.GetBuffer().AsIoBuffer();
- const uint32_t OpCoreHash = uint32_t(XXH3_64bits(Buffer.GetData(), Buffer.GetSize()) & 0xffffFFFF);
- Ops.insert({Id, OpCoreHash});
- };
-
- auto MakeProject = [](HttpClient& Client, std::string_view ProjectName) {
- CbObjectWriter Project;
- Project.AddString("id"sv, ProjectName);
- Project.AddString("root"sv, ""sv);
- Project.AddString("engine"sv, ""sv);
- Project.AddString("project"sv, ""sv);
- Project.AddString("projectfile"sv, ""sv);
- HttpClient::Response Response = Client.Post(fmt::format("/prj/{}", ProjectName), Project.Save());
- CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
- };
-
- auto MakeOplog = [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName) {
- HttpClient::Response Response = Client.Post(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName));
- CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
- };
- auto GetOplog = [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName) {
- HttpClient::Response Response = Client.Get(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName));
- CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
- return Response.AsObject();
- };
-
- auto MakeOp =
- [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName, const CbObjectView& Op) -> std::vector<IoHash> {
- CbObjectWriter Request;
- Request.AddString("method"sv, "appendops"sv);
- Request.BeginArray("ops"sv);
- {
- Request.AddObject(Op);
- }
- Request.EndArray(); // "ops"
- HttpClient::Response Response = Client.Post(fmt::format("/prj/{}/oplog/{}/rpc", ProjectName, OplogName), Request.Save());
- CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
-
- CbObjectView ResponsePayload = Response.AsPackage().GetObject();
- CbArrayView NeedArray = ResponsePayload["need"sv].AsArrayView();
- std::vector<IoHash> Needs;
- Needs.reserve(NeedArray.Num());
- for (CbFieldView NeedView : NeedArray)
- {
- Needs.push_back(NeedView.AsHash());
- }
- return Needs;
- };
-
- auto SendAttachments = [](HttpClient& Client,
- std::string_view ProjectName,
- std::string_view OplogName,
- std::span<const CompressedBuffer> Attachments,
- void* ServerProcessHandle,
- const std::filesystem::path& TempPath) {
- CompositeBuffer PackageMessage;
- {
- CbPackage RequestPackage;
- CbObjectWriter Request;
- Request.AddString("method"sv, "putchunks"sv);
- Request.AddBool("usingtmpfiles"sv, true);
- Request.BeginArray("chunks"sv);
- for (CompressedBuffer AttachmentPayload : Attachments)
- {
- if (AttachmentPayload.DecodeRawSize() > 16u * 1024u)
- {
- std::filesystem::path TempAttachmentPath = TempPath / (Oid::NewOid().ToString() + ".tmp");
- WriteFile(TempAttachmentPath, AttachmentPayload.GetCompressed());
- IoBuffer OnDiskAttachment = IoBufferBuilder::MakeFromFile(TempAttachmentPath);
- AttachmentPayload = CompressedBuffer::FromCompressedNoValidate(std::move(OnDiskAttachment));
- }
-
- CbAttachment Attachment(AttachmentPayload, AttachmentPayload.DecodeRawHash());
-
- Request.AddAttachment(Attachment);
- RequestPackage.AddAttachment(Attachment);
- }
- Request.EndArray(); // "chunks"
- RequestPackage.SetObject(Request.Save());
-
- PackageMessage = CompositeBuffer(FormatPackageMessage(RequestPackage, FormatFlags::kAllowLocalReferences, ServerProcessHandle));
- }
-
- HttpClient::Response Response =
- Client.Post(fmt::format("/prj/{}/oplog/{}/rpc", ProjectName, OplogName), PackageMessage, HttpContentType::kCbPackage);
- CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage(""));
- };
-
- {
- HttpClient Client(Servers.GetInstance(0).GetBaseUri());
- void* ServerProcessHandle = Servers.GetInstance(0).GetProcessHandle();
-
- MakeProject(Client, "proj0");
- MakeOplog(Client, "proj0", "oplog0");
- CbObject Oplog = GetOplog(Client, "proj0", "oplog0");
- std::filesystem::path TempPath = Oplog["tempdir"sv].AsU8String();
-
- std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps;
- for (const Oid& OpId : OpIds)
- {
- CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
- AddOp(Op, SourceOps);
- std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
-
- if (!MissingAttachments.empty())
- {
- CHECK(MissingAttachments.size() <= Attachments[OpId].size());
- tsl::robin_set<IoHash, IoHash::Hasher> MissingAttachmentSet(MissingAttachments.begin(), MissingAttachments.end());
- std::vector<CompressedBuffer> PutAttachments;
- for (const auto& Attachment : Attachments[OpId])
- {
- CompressedBuffer Payload = Attachment.second;
- const IoHash AttachmentHash = Payload.DecodeRawHash();
- if (auto It = MissingAttachmentSet.find(AttachmentHash); It != MissingAttachmentSet.end())
- {
- PutAttachments.push_back(Payload);
- }
- }
- SendAttachments(Client, "proj0", "oplog0", PutAttachments, ServerProcessHandle, TempPath);
- }
- }
-
- // Do it again, but now we should not need any attachments
-
- for (const Oid& OpId : OpIds)
- {
- CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
- AddOp(Op, SourceOps);
- std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
- CHECK(MissingAttachments.empty());
- }
- }
-
- {
- HttpClient Client(Servers.GetInstance(1).GetBaseUri());
- void* ServerProcessHandle = nullptr; // Force use of path for attachments passed on disk
-
- MakeProject(Client, "proj0");
- MakeOplog(Client, "proj0", "oplog0");
- CbObject Oplog = GetOplog(Client, "proj0", "oplog0");
- std::filesystem::path TempPath = Oplog["tempdir"sv].AsU8String();
-
- std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps;
- for (const Oid& OpId : OpIds)
- {
- CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
- AddOp(Op, SourceOps);
- std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
-
- if (!MissingAttachments.empty())
- {
- CHECK(MissingAttachments.size() <= Attachments[OpId].size());
- tsl::robin_set<IoHash, IoHash::Hasher> MissingAttachmentSet(MissingAttachments.begin(), MissingAttachments.end());
- std::vector<CompressedBuffer> PutAttachments;
- for (const auto& Attachment : Attachments[OpId])
- {
- CompressedBuffer Payload = Attachment.second;
- const IoHash AttachmentHash = Payload.DecodeRawHash();
- if (auto It = MissingAttachmentSet.find(AttachmentHash); It != MissingAttachmentSet.end())
- {
- PutAttachments.push_back(Payload);
- }
- }
- SendAttachments(Client, "proj0", "oplog0", PutAttachments, ServerProcessHandle, TempPath);
- }
- }
-
- // Do it again, but now we should not need any attachments
-
- for (const Oid& OpId : OpIds)
- {
- CbObject Op = CreateOplogOp(OpId, Attachments[OpId]);
- AddOp(Op, SourceOps);
- std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op);
- CHECK(MissingAttachments.empty());
- }
- }
-}
-
-std::vector<std::pair<std::filesystem::path, IoBuffer>>
-GenerateFolderContent(const std::filesystem::path& RootPath)
-{
- CreateDirectories(RootPath);
- std::vector<std::pair<std::filesystem::path, IoBuffer>> Result;
- Result.push_back(std::make_pair(RootPath / "root_blob_1.bin", CreateRandomBlob(4122)));
- Result.push_back(std::make_pair(RootPath / "root_blob_2.bin", CreateRandomBlob(2122)));
-
- std::filesystem::path EmptyFolder(RootPath / "empty_folder");
-
- std::filesystem::path FirstFolder(RootPath / "first_folder");
- CreateDirectories(FirstFolder);
- Result.push_back(std::make_pair(FirstFolder / "first_folder_blob1.bin", CreateRandomBlob(22)));
- Result.push_back(std::make_pair(FirstFolder / "first_folder_blob2.bin", CreateRandomBlob(122)));
-
- std::filesystem::path SecondFolder(RootPath / "second_folder");
- CreateDirectories(SecondFolder);
- Result.push_back(std::make_pair(SecondFolder / "second_folder_blob1.bin", CreateRandomBlob(522)));
- Result.push_back(std::make_pair(SecondFolder / "second_folder_blob2.bin", CreateRandomBlob(122)));
- Result.push_back(std::make_pair(SecondFolder / "second_folder_blob3.bin", CreateRandomBlob(225)));
-
- std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second");
- CreateDirectories(SecondFolderChild);
- Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob1.bin", CreateRandomBlob(622)));
-
- for (const auto& It : Result)
- {
- WriteFile(It.first, It.second);
- }
-
- return Result;
-}
-
-std::vector<std::pair<std::filesystem::path, IoBuffer>>
-GenerateFolderContent2(const std::filesystem::path& RootPath)
-{
- std::vector<std::pair<std::filesystem::path, IoBuffer>> Result;
- Result.push_back(std::make_pair(RootPath / "root_blob_3.bin", CreateRandomBlob(312)));
- std::filesystem::path FirstFolder(RootPath / "first_folder");
- Result.push_back(std::make_pair(FirstFolder / "first_folder_blob3.bin", CreateRandomBlob(722)));
- std::filesystem::path SecondFolder(RootPath / "second_folder");
- std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second");
- Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob2.bin", CreateRandomBlob(962)));
- Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob3.bin", CreateRandomBlob(561)));
-
- for (const auto& It : Result)
- {
- WriteFile(It.first, It.second);
- }
-
- return Result;
-}
-
-TEST_CASE("workspaces.create")
-{
- using namespace std::literals;
-
- std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
-
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
- ZenServerInstance Instance(TestEnv);
- Instance.SetTestDir(TestDir);
- const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
- fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- ScopedTemporaryDirectory TempDir;
- std::filesystem::path Root1Path = TempDir.Path() / "root1";
- std::filesystem::path Root2Path = TempDir.Path() / "root2";
- DeleteDirectories(Root1Path);
- DeleteDirectories(Root2Path);
-
- std::filesystem::path Share1Path = "shared_1";
- std::filesystem::path Share2Path = "shared_2";
- CreateDirectories(Root1Path / Share1Path);
- CreateDirectories(Root1Path / Share2Path);
- CreateDirectories(Root2Path / Share1Path);
- CreateDirectories(Root2Path / Share2Path);
-
- Oid Root1Id = Oid::Zero;
- Oid Root2Id = Oid::NewOid();
-
- HttpClient Client(Instance.GetBaseUri());
-
- CHECK(Client.Put(fmt::format("/ws/{}", Root1Id)).StatusCode == HttpResponseCode::BadRequest);
-
- if (HttpClient::Response Root1Response =
- Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
- Root1Response.StatusCode == HttpResponseCode::Created)
- {
- Root1Id = Oid::TryFromHexString(Root1Response.AsText());
- CHECK(Root1Id != Oid::Zero);
- }
- else
- {
- CHECK(false);
- }
- if (HttpClient::Response Root1Response =
- Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
- Root1Response.StatusCode == HttpResponseCode::OK)
- {
- CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText()));
- }
- else
- {
- CHECK(false);
- }
- if (HttpClient::Response Root1Response =
- Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}});
- Root1Response.StatusCode == HttpResponseCode::OK)
- {
- CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText()));
- }
- else
- {
- CHECK(false);
- }
- CHECK(Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}).StatusCode ==
- HttpResponseCode::Conflict);
-
- CHECK(
- Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
- HttpResponseCode::Created);
-
- CHECK(
- Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
- HttpResponseCode::NotFound);
-
- CHECK(Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode ==
- HttpResponseCode::Conflict);
-
- if (HttpClient::Response Root2Response =
- Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}});
- Root2Response.StatusCode == HttpResponseCode::Created)
- {
- CHECK(Root2Id == Oid::TryFromHexString(Root2Response.AsText()));
- }
- else
- {
- CHECK(false);
- }
-
- CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero)).StatusCode == HttpResponseCode::BadRequest);
-
- Oid Share2Id = Oid::Zero;
- if (HttpClient::Response Share2Response =
- Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}});
- Share2Response.StatusCode == HttpResponseCode::Created)
- {
- Share2Id = Oid::TryFromHexString(Share2Response.AsText());
- CHECK(Share2Id != Oid::Zero);
- }
- else
- {
- CHECK(false);
- }
-
- CHECK(
- Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
- HttpResponseCode::OK);
-
- CHECK(
- Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
- HttpResponseCode::OK);
-
- CHECK(
- Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode ==
- HttpResponseCode::Conflict);
-
- CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::NewOid()), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}})
- .StatusCode == HttpResponseCode::Conflict);
-
- CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", "idonotexist"}}).StatusCode !=
- HttpResponseCode::OK);
-
- while (true)
- {
- std::error_code Ec;
- DeleteDirectories(Root2Path / Share2Path, Ec);
- if (!Ec)
- break;
- }
-
- CHECK(Client.Get(fmt::format("/ws/{}/{}/files", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound);
-}
-
-TEST_CASE("workspaces.restricted")
-{
- using namespace std::literals;
-
- std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
-
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
- ZenServerInstance Instance(TestEnv);
- Instance.SetTestDir(TestDir);
- const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- ScopedTemporaryDirectory TempDir;
- std::filesystem::path Root1Path = TempDir.Path() / "root1";
- std::filesystem::path Root2Path = TempDir.Path() / "root2";
- DeleteDirectories(Root1Path);
- DeleteDirectories(Root2Path);
-
- std::filesystem::path Share1Path = "shared_1";
- std::filesystem::path Share2Path = "shared_2";
- CreateDirectories(Root1Path / Share1Path);
- CreateDirectories(Root1Path / Share2Path);
- CreateDirectories(Root2Path / Share1Path);
- CreateDirectories(Root2Path / Share2Path);
-
- Oid Root1Id = Oid::NewOid();
- Oid Root2Id = Oid::NewOid();
- Oid Share1Id = Oid::NewOid();
- Oid Share2Id = Oid::NewOid();
-
- HttpClient Client(Instance.GetBaseUri());
- CHECK(Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode ==
- HttpResponseCode::Unauthorized);
-
- CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::NotFound);
-
- std::string Config1;
- {
- CbObjectWriter Config;
- Config.BeginArray("workspaces");
- Config.BeginObject();
- Config << "id"sv << Root1Id.ToString();
- Config << "root_path"sv << Root1Path.string();
- Config << "allow_share_creation_from_http"sv << false;
- Config.EndObject();
- Config.EndArray();
- ExtendableStringBuilder<256> SB;
- CompactBinaryToJson(Config.Save(), SB);
- Config1 = SB.ToString();
- }
- WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config1.data(), Config1.size()));
-
- CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode));
-
- CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::OK);
-
- CHECK(Client.Get(fmt::format("/ws/{}/{}", Root1Id, Share1Id)).StatusCode == HttpResponseCode::NotFound);
- CHECK(
- Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode ==
- HttpResponseCode::Unauthorized);
-
- std::string Config2;
- {
- CbObjectWriter Config;
- Config.BeginArray("workspaces");
- Config.BeginObject();
- Config << "id"sv << Root1Id.ToString();
- Config << "root_path"sv << Root1Path.string();
- Config << "allow_share_creation_from_http"sv << false;
- Config.EndObject();
- Config.BeginObject();
- Config << "id"sv << Root2Id.ToString();
- Config << "root_path"sv << Root2Path.string();
- Config << "allow_share_creation_from_http"sv << true;
- Config.EndObject();
- Config.EndArray();
- ExtendableStringBuilder<256> SB;
- CompactBinaryToJson(Config.Save(), SB);
- Config2 = SB.ToString();
- }
- WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config2.data(), Config2.size()));
-
- CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode));
-
- CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root2Id)).StatusCode, HttpResponseCode::OK);
-
- CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound);
- CHECK(
- Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode ==
- HttpResponseCode::Created);
- CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::OK);
-
- CHECK(IsHttpSuccessCode(Client.Delete(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode));
-}
-
-TEST_CASE("workspaces.lifetimes")
-{
- using namespace std::literals;
-
- std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
-
- Oid WorkspaceId = Oid::NewOid();
- Oid ShareId = Oid::NewOid();
-
- ScopedTemporaryDirectory TempDir;
- std::filesystem::path RootPath = TempDir.Path();
- DeleteDirectories(RootPath);
- std::filesystem::path SharePath = RootPath / "shared_folder";
- CreateDirectories(SharePath);
-
- {
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
- ZenServerInstance Instance(TestEnv);
- Instance.SetTestDir(TestDir);
- const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
- fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri());
- CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
- HttpResponseCode::Created);
- CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
- CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
- HttpResponseCode::OK);
-
- CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}})
- .StatusCode == HttpResponseCode::Created);
- CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
- CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}})
- .StatusCode == HttpResponseCode::OK);
- }
-
- // Restart
-
- {
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
- ZenServerInstance Instance(TestEnv);
- Instance.SetTestDir(TestDir);
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri());
- CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
-
- CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
- }
-
- // Wipe system config
- DeleteDirectories(SystemRootPath);
-
- // Restart
-
- {
- std::filesystem::path TestDir = TestEnv.CreateNewTestDir();
- ZenServerInstance Instance(TestEnv);
- Instance.SetTestDir(TestDir);
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri());
- CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound);
- CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound);
- }
-}
-
-TEST_CASE("workspaces.share")
-{
- std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
-
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(
- fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- ScopedTemporaryDirectory TempDir;
- std::filesystem::path RootPath = TempDir.Path();
- DeleteDirectories(RootPath);
- std::filesystem::path SharePath = RootPath / "shared_folder";
- GenerateFolderContent(SharePath);
-
- HttpClient Client(Instance.GetBaseUri());
-
- Oid WorkspaceId = Oid::NewOid();
- CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode ==
- HttpResponseCode::Created);
- CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId);
-
- Oid ShareId = Oid::NewOid();
- CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}).StatusCode ==
- HttpResponseCode::Created);
- CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId);
-
- CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8);
- GenerateFolderContent2(SharePath);
- CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8);
- HttpClient::Response FilesResponse =
- Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId),
- {},
- HttpClient::KeyValueMap{{"refresh", ToString(true)}, {"fieldnames", "id,clientpath,size"}});
- CHECK(FilesResponse);
- std::unordered_map<Oid, std::pair<std::filesystem::path, uint64_t>, Oid::Hasher> Files;
- {
- CbArrayView FilesArray = FilesResponse.AsObject()["files"sv].AsArrayView();
- CHECK(FilesArray.Num() == 12);
- for (CbFieldView Field : FilesArray)
- {
- CbObjectView FileObject = Field.AsObjectView();
- Oid ChunkId = FileObject["id"sv].AsObjectId();
- CHECK(ChunkId != Oid::Zero);
- uint64_t Size = FileObject["size"sv].AsUInt64();
- std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
- std::filesystem::path AbsFilePath = SharePath / Path;
- CHECK(IsFile(AbsFilePath));
- CHECK(FileSizeFromPath(AbsFilePath) == Size);
- Files.insert_or_assign(ChunkId, std::make_pair(AbsFilePath, Size));
- }
- }
-
- HttpClient::Response EntriesResponse =
- Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId), {}, HttpClient::KeyValueMap{{"fieldfilter", "id,clientpath"}});
- CHECK(EntriesResponse);
- {
- CbArrayView EntriesArray = EntriesResponse.AsObject()["entries"sv].AsArrayView();
- CHECK(EntriesArray.Num() == 1);
- for (CbFieldView EntryField : EntriesArray)
- {
- CbObjectView EntryObject = EntryField.AsObjectView();
- CbArrayView FilesArray = EntryObject["files"sv].AsArrayView();
- CHECK(FilesArray.Num() == 12);
- for (CbFieldView FileField : FilesArray)
- {
- CbObjectView FileObject = FileField.AsObjectView();
- Oid ChunkId = FileObject["id"sv].AsObjectId();
- CHECK(ChunkId != Oid::Zero);
- std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
- std::filesystem::path AbsFilePath = SharePath / Path;
- CHECK(IsFile(AbsFilePath));
- }
- }
- }
-
- HttpClient::Response FileManifestResponse =
- Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId),
- {},
- HttpClient::KeyValueMap{{"opkey", "file_manifest"}, {"fieldfilter", "id,clientpath"}});
- CHECK(FileManifestResponse);
- {
- CbArrayView EntriesArray = FileManifestResponse.AsObject()["entry"sv].AsObjectView()["files"sv].AsArrayView();
- CHECK(EntriesArray.Num() == 12);
- for (CbFieldView Field : EntriesArray)
- {
- CbObjectView FileObject = Field.AsObjectView();
- Oid ChunkId = FileObject["id"sv].AsObjectId();
- CHECK(ChunkId != Oid::Zero);
- std::u8string_view Path = FileObject["clientpath"sv].AsU8String();
- std::filesystem::path AbsFilePath = SharePath / Path;
- CHECK(IsFile(AbsFilePath));
- }
- }
-
- for (auto It : Files)
- {
- const Oid& ChunkId = It.first;
- const std::filesystem::path& Path = It.second.first;
- const uint64_t Size = It.second.second;
-
- CHECK(Client.Get(fmt::format("/ws/{}/{}/{}/info", WorkspaceId, ShareId, ChunkId)).AsObject()["size"sv].AsUInt64() == Size);
-
- {
- IoBuffer Payload = Client.Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId)).ResponsePayload;
- CHECK(Payload);
- CHECK(Payload.GetSize() == Size);
- IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path);
- CHECK(FileContent);
- CHECK(FileContent.GetView().EqualBytes(Payload.GetView()));
- }
-
- {
- IoBuffer Payload =
- Client
- .Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId),
- {},
- HttpClient::KeyValueMap{{"offset", fmt::format("{}", Size / 4)}, {"size", fmt::format("{}", Size / 2)}})
- .ResponsePayload;
- CHECK(Payload);
- CHECK(Payload.GetSize() == Size / 2);
- IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Size / 4, Size / 2);
- CHECK(FileContent);
- CHECK(FileContent.GetView().EqualBytes(Payload.GetView()));
- }
- }
-
- {
- uint32_t CorrelationId = gsl::narrow<uint32_t>(Files.size());
- std::vector<RequestChunkEntry> BatchEntries;
- for (auto It : Files)
- {
- const Oid& ChunkId = It.first;
- const uint64_t Size = It.second.second;
-
- BatchEntries.push_back(
- RequestChunkEntry{.ChunkId = ChunkId, .CorrelationId = --CorrelationId, .Offset = Size / 4, .RequestBytes = Size / 2});
- }
- IoBuffer BatchResponse =
- Client.Post(fmt::format("/ws/{}/{}/batch", WorkspaceId, ShareId), BuildChunkBatchRequest(BatchEntries)).ResponsePayload;
- CHECK(BatchResponse);
- std::vector<IoBuffer> BatchResult = ParseChunkBatchResponse(BatchResponse);
- CHECK(BatchResult.size() == Files.size());
- for (const RequestChunkEntry& Request : BatchEntries)
- {
- IoBuffer Result = BatchResult[Request.CorrelationId];
- auto It = Files.find(Request.ChunkId);
- const std::filesystem::path& Path = It->second.first;
- CHECK(Result.GetSize() == Request.RequestBytes);
- IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Request.Offset, Request.RequestBytes);
- CHECK(FileContent);
- CHECK(FileContent.GetView().EqualBytes(Result.GetView()));
- }
- }
-
- CHECK(Client.Delete(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)));
- CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound);
- CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)));
-
- CHECK(Client.Delete(fmt::format("/ws/{}", WorkspaceId)));
- CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound);
-}
-
-TEST_CASE("buildstore.blobs")
-{
- std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
- auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); });
-
- std::string_view Namespace = "ns"sv;
- std::string_view Bucket = "bkt"sv;
- Oid BuildId = Oid::NewOid();
-
- std::vector<IoHash> CompressedBlobsHashes;
- {
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri() + "/builds/");
-
- for (size_t I = 0; I < 5; I++)
- {
- IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7);
- CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
- CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash());
- IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer();
- Payload.SetContentType(ZenContentType::kCompressedBinary);
-
- HttpClient::Response Result =
- Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload);
- CHECK(Result);
- }
-
- for (const IoHash& RawHash : CompressedBlobsHashes)
- {
- HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash),
- HttpClient::Accept(ZenContentType::kCompressedBinary));
- CHECK(Result);
- IoBuffer Payload = Result.ResponsePayload;
- CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary);
- IoHash VerifyRawHash;
- uint64_t VerifyRawSize;
- CompressedBuffer CompressedBlob =
- CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize);
- CHECK(CompressedBlob);
- CHECK(VerifyRawHash == RawHash);
- IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer();
- CHECK(IoHash::HashBuffer(Decompressed) == RawHash);
- }
- }
- {
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri() + "/builds/");
-
- for (const IoHash& RawHash : CompressedBlobsHashes)
- {
- HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash),
- HttpClient::Accept(ZenContentType::kCompressedBinary));
- CHECK(Result);
- IoBuffer Payload = Result.ResponsePayload;
- CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary);
- IoHash VerifyRawHash;
- uint64_t VerifyRawSize;
- CompressedBuffer CompressedBlob =
- CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize);
- CHECK(CompressedBlob);
- CHECK(VerifyRawHash == RawHash);
- IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer();
- CHECK(IoHash::HashBuffer(Decompressed) == RawHash);
- }
-
- for (size_t I = 0; I < 5; I++)
- {
- IoBuffer Blob = CreateSemiRandomBlob(5713 + I * 7);
- CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
- CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash());
- IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer();
- Payload.SetContentType(ZenContentType::kCompressedBinary);
-
- HttpClient::Response Result =
- Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload);
- CHECK(Result);
- }
- }
- {
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri() + "/builds/");
-
- for (const IoHash& RawHash : CompressedBlobsHashes)
- {
- HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash),
- HttpClient::Accept(ZenContentType::kCompressedBinary));
- CHECK(Result);
- IoBuffer Payload = Result.ResponsePayload;
- CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary);
- IoHash VerifyRawHash;
- uint64_t VerifyRawSize;
- CompressedBuffer CompressedBlob =
- CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize);
- CHECK(CompressedBlob);
- CHECK(VerifyRawHash == RawHash);
- IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer();
- CHECK(IoHash::HashBuffer(Decompressed) == RawHash);
- }
- }
-}
-
-namespace {
- CbObject MakeMetadata(const IoHash& BlobHash, const std::vector<std::pair<std::string, std::string>>& KeyValues)
- {
- CbObjectWriter Writer;
- Writer.AddHash("rawHash"sv, BlobHash);
- Writer.BeginObject("values");
- {
- for (const auto& V : KeyValues)
- {
- Writer.AddString(V.first, V.second);
- }
- }
- Writer.EndObject(); // values
- return Writer.Save();
- };
-
-} // namespace
-
-TEST_CASE("buildstore.metadata")
-{
- std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
- auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); });
-
- std::string_view Namespace = "ns"sv;
- std::string_view Bucket = "bkt"sv;
- Oid BuildId = Oid::NewOid();
-
- std::vector<IoHash> BlobHashes;
- std::vector<CbObject> Metadatas;
- std::vector<IoHash> MetadataHashes;
-
- auto GetMetadatas =
- [](HttpClient& Client, std::string_view Namespace, std::string_view Bucket, const Oid& BuildId, std::vector<IoHash> BlobHashes) {
- CbObjectWriter Request;
-
- Request.BeginArray("blobHashes"sv);
- for (const IoHash& BlobHash : BlobHashes)
- {
- Request.AddHash(BlobHash);
- }
- Request.EndArray();
-
- IoBuffer Payload = Request.Save().GetBuffer().AsIoBuffer();
- Payload.SetContentType(ZenContentType::kCbObject);
-
- HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/getBlobMetadata", Namespace, Bucket, BuildId),
- Payload,
- HttpClient::Accept(ZenContentType::kCbObject));
- CHECK(Result);
-
- std::vector<CbObject> ResultMetadatas;
-
- CbPackage ResponsePackage = ParsePackageMessage(Result.ResponsePayload);
- CbObject ResponseObject = ResponsePackage.GetObject();
-
- CbArrayView BlobHashArray = ResponseObject["blobHashes"sv].AsArrayView();
- CbArrayView MetadatasArray = ResponseObject["metadatas"sv].AsArrayView();
- ResultMetadatas.reserve(MetadatasArray.Num());
- auto BlobHashesIt = BlobHashes.begin();
- auto BlobHashArrayIt = begin(BlobHashArray);
- auto MetadataArrayIt = begin(MetadatasArray);
- while (MetadataArrayIt != end(MetadatasArray))
- {
- const IoHash BlobHash = (*BlobHashArrayIt).AsHash();
- while (BlobHash != *BlobHashesIt)
- {
- ZEN_ASSERT(BlobHashesIt != BlobHashes.end());
- BlobHashesIt++;
- }
-
- ZEN_ASSERT(BlobHash == *BlobHashesIt);
-
- const IoHash MetaHash = (*MetadataArrayIt).AsAttachment();
- const CbAttachment* MetaAttachment = ResponsePackage.FindAttachment(MetaHash);
- ZEN_ASSERT(MetaAttachment);
-
- CbObject Metadata = MetaAttachment->AsObject();
- ResultMetadatas.emplace_back(std::move(Metadata));
-
- BlobHashArrayIt++;
- MetadataArrayIt++;
- BlobHashesIt++;
- }
- return ResultMetadatas;
- };
-
- {
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri() + "/builds/");
-
- const size_t BlobCount = 5;
-
- for (size_t I = 0; I < BlobCount; I++)
- {
- BlobHashes.push_back(IoHash::HashBuffer(&I, sizeof(I)));
- Metadatas.push_back(MakeMetadata(BlobHashes.back(), {{"index", fmt::format("{}", I)}}));
- MetadataHashes.push_back(IoHash::HashBuffer(Metadatas.back().GetBuffer().AsIoBuffer()));
- }
-
- {
- CbPackage RequestPackage;
- std::vector<CbAttachment> Attachments;
- tsl::robin_set<IoHash, IoHash::Hasher> AttachmentHashes;
- Attachments.reserve(BlobCount);
- AttachmentHashes.reserve(BlobCount);
- {
- CbObjectWriter RequestWriter;
- RequestWriter.BeginArray("blobHashes");
- for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++)
- {
- RequestWriter.AddHash(BlobHashes[BlockHashIndex]);
- }
- RequestWriter.EndArray(); // blobHashes
-
- RequestWriter.BeginArray("metadatas");
- for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++)
- {
- const IoHash ObjectHash = Metadatas[BlockHashIndex].GetHash();
- RequestWriter.AddBinaryAttachment(ObjectHash);
- if (!AttachmentHashes.contains(ObjectHash))
- {
- Attachments.push_back(CbAttachment(Metadatas[BlockHashIndex], ObjectHash));
- AttachmentHashes.insert(ObjectHash);
- }
- }
-
- RequestWriter.EndArray(); // metadatas
-
- RequestPackage.SetObject(RequestWriter.Save());
- }
- RequestPackage.AddAttachments(Attachments);
-
- CompositeBuffer RpcRequestBuffer = FormatPackageMessageBuffer(RequestPackage);
-
- HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/putBlobMetadata", Namespace, Bucket, BuildId),
- RpcRequestBuffer,
- ZenContentType::kCbPackage);
- CHECK(Result);
- }
-
- {
- std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes);
-
- for (size_t Index = 0; Index < MetadataHashes.size(); Index++)
- {
- const IoHash& ExpectedHash = MetadataHashes[Index];
- IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer());
- CHECK_EQ(ExpectedHash, Hash);
- }
- }
- }
- {
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri() + "/builds/");
-
- std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes);
-
- for (size_t Index = 0; Index < MetadataHashes.size(); Index++)
- {
- const IoHash& ExpectedHash = MetadataHashes[Index];
- IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer());
- CHECK_EQ(ExpectedHash, Hash);
- }
- }
-}
-
-TEST_CASE("buildstore.cache")
-{
- std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir();
- std::filesystem::path TempDir = TestEnv.CreateNewTestDir();
- auto _ = MakeGuard([&SystemRootPath, &TempDir]() {
- DeleteDirectories(SystemRootPath);
- DeleteDirectories(TempDir);
- });
-
- std::string_view Namespace = "ns"sv;
- std::string_view Bucket = "bkt"sv;
- Oid BuildId = Oid::NewOid();
-
- std::vector<IoHash> BlobHashes;
- std::vector<CbObject> Metadatas;
- std::vector<IoHash> MetadataHashes;
-
- const size_t BlobCount = 5;
- {
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri());
-
- BuildStorageCache::Statistics Stats;
- std::unique_ptr<BuildStorageCache> Cache(CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false));
-
- {
- IoHash NoneBlob = IoHash::HashBuffer("data", 4);
- std::vector<BuildStorageCache::BlobExistsResult> NoneExists = Cache->BlobsExists(BuildId, std::vector<IoHash>{NoneBlob});
- CHECK(NoneExists.size() == 1);
- CHECK(!NoneExists[0].HasBody);
- CHECK(!NoneExists[0].HasMetadata);
- }
-
- for (size_t I = 0; I < BlobCount; I++)
- {
- IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7);
- CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
- BlobHashes.push_back(CompressedBlob.DecodeRawHash());
- Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed());
- }
-
- Cache->Flush(500);
- Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false);
-
- {
- std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
- CHECK(Exists.size() == BlobHashes.size());
- for (size_t I = 0; I < BlobCount; I++)
- {
- CHECK(Exists[I].HasBody);
- CHECK(!Exists[I].HasMetadata);
- }
-
- std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
- CHECK_EQ(0, FetchedMetadatas.size());
- }
-
- {
- for (size_t I = 0; I < BlobCount; I++)
- {
- IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]);
- CHECK(BuildBlob);
- CHECK_EQ(BlobHashes[I],
- IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer()));
- }
- }
-
- {
- for (size_t I = 0; I < BlobCount; I++)
- {
- CbObject Metadata = MakeMetadata(BlobHashes[I],
- {{"key", fmt::format("{}", I)},
- {"key_plus_one", fmt::format("{}", I + 1)},
- {"block_hash", fmt::format("{}", BlobHashes[I])}});
- Metadatas.push_back(Metadata);
- MetadataHashes.push_back(IoHash::HashBuffer(Metadata.GetBuffer().AsIoBuffer()));
- }
- Cache->PutBlobMetadatas(BuildId, BlobHashes, Metadatas);
- }
-
- Cache->Flush(500);
- Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false);
-
- {
- std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
- CHECK(Exists.size() == BlobHashes.size());
- for (size_t I = 0; I < BlobCount; I++)
- {
- CHECK(Exists[I].HasBody);
- CHECK(Exists[I].HasMetadata);
- }
-
- std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
- CHECK_EQ(BlobCount, FetchedMetadatas.size());
-
- for (size_t I = 0; I < BlobCount; I++)
- {
- CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer()));
- }
- }
-
- for (size_t I = 0; I < BlobCount; I++)
- {
- IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7);
- CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob)));
- BlobHashes.push_back(CompressedBlob.DecodeRawHash());
- Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed());
- }
-
- Cache->Flush(500);
- Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false);
-
- {
- std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
- CHECK(Exists.size() == BlobHashes.size());
- for (size_t I = 0; I < BlobCount * 2; I++)
- {
- CHECK(Exists[I].HasBody);
- CHECK_EQ(I < BlobCount, Exists[I].HasMetadata);
- }
-
- std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
- CHECK_EQ(BlobCount, MetaDatas.size());
-
- std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
- CHECK_EQ(BlobCount, FetchedMetadatas.size());
-
- for (size_t I = 0; I < BlobCount; I++)
- {
- CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer()));
- }
- }
- }
-
- {
- ZenServerInstance Instance(TestEnv);
-
- const uint16_t PortNumber =
- Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath));
- CHECK(PortNumber != 0);
-
- HttpClient Client(Instance.GetBaseUri());
-
- BuildStorageCache::Statistics Stats;
- std::unique_ptr<BuildStorageCache> Cache(CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false));
-
- std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes);
- CHECK(Exists.size() == BlobHashes.size());
- for (size_t I = 0; I < BlobCount * 2; I++)
- {
- CHECK(Exists[I].HasBody);
- CHECK_EQ(I < BlobCount, Exists[I].HasMetadata);
- }
-
- for (size_t I = 0; I < BlobCount * 2; I++)
- {
- IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]);
- CHECK(BuildBlob);
- CHECK_EQ(BlobHashes[I],
- IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer()));
- }
-
- std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
- CHECK_EQ(BlobCount, MetaDatas.size());
-
- std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes);
- CHECK_EQ(BlobCount, FetchedMetadatas.size());
-
- for (size_t I = 0; I < BlobCount; I++)
- {
- CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer()));
- }
- }
-}
-
# if 0
TEST_CASE("lifetime.owner")
{
diff --git a/src/zenserver-test/zenserver-test.h b/src/zenserver-test/zenserver-test.h
new file mode 100644
index 000000000..e7cee3f94
--- /dev/null
+++ b/src/zenserver-test/zenserver-test.h
@@ -0,0 +1,207 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#if ZEN_WITH_TESTS
+
+# include <zencore/compactbinarybuilder.h>
+# include <zencore/compactbinarypackage.h>
+# include <zencore/iobuffer.h>
+# include <zencore/stream.h>
+# include <zencore/string.h>
+# include <zencore/testing.h>
+# include <zencore/testutils.h>
+# include <zenhttp/httpcommon.h>
+# include <zenutil/zenserverprocess.h>
+
+# include <functional>
+
+namespace zen::tests {
+
+extern zen::ZenServerEnvironment TestEnv;
+
+inline IoBuffer
+MakeCbObjectPayload(std::function<void(CbObjectWriter& Writer)> WriteCB)
+{
+ CbObjectWriter Writer;
+ WriteCB(Writer);
+ IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer();
+ Payload.SetContentType(ZenContentType::kCbObject);
+ return Payload;
+};
+
+inline IoBuffer
+SerializeToBuffer(const zen::CbPackage& Package)
+{
+ BinaryWriter MemStream;
+
+ Package.Save(MemStream);
+
+ IoBuffer Buffer = zen::IoBuffer(zen::IoBuffer::Clone, MemStream.Data(), MemStream.Size());
+ Buffer.SetContentType(HttpContentType::kCbPackage);
+ return Buffer;
+};
+
+namespace utils {
+
+ struct ZenConfig
+ {
+ std::filesystem::path DataDir;
+ uint16_t Port;
+ std::string BaseUri;
+ std::string Args;
+
+ static ZenConfig New(std::string Args = "")
+ {
+ return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = TestEnv.GetNewPortNumber(), .Args = std::move(Args)};
+ }
+
+ static ZenConfig New(uint16_t Port, std::string Args = "")
+ {
+ return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = Port, .Args = std::move(Args)};
+ }
+
+ static ZenConfig NewWithUpstream(uint16_t Port, uint16_t UpstreamPort, std::string Args = "")
+ {
+ return New(Port,
+ fmt::format("{}{}--debug --upstream-thread-count=0 --upstream-zen-url=http://localhost:{}",
+ Args,
+ Args.length() > 0 ? " " : "",
+ UpstreamPort));
+ }
+
+ static ZenConfig NewWithThreadedUpstreams(uint16_t NewPort, std::span<uint16_t> UpstreamPorts, bool Debug)
+ {
+ std::string Args = Debug ? "--debug" : "";
+ for (uint16_t Port : UpstreamPorts)
+ {
+ Args = fmt::format("{}{}--upstream-zen-url=http://localhost:{}", Args, Args.length() > 0 ? " " : "", Port);
+ }
+ return New(NewPort, Args);
+ }
+
+ void Spawn(ZenServerInstance& Inst)
+ {
+ Inst.SetTestDir(DataDir);
+ Inst.SpawnServer(Port, Args);
+ const uint16_t InstancePort = Inst.WaitUntilReady();
+ CHECK_MESSAGE(InstancePort != 0, Inst.GetLogOutput());
+
+ if (Port != InstancePort)
+ ZEN_DEBUG("relocation detected from {} to {}", Port, InstancePort);
+
+ Port = InstancePort;
+ BaseUri = fmt::format("http://localhost:{}/z$", Port);
+ }
+ };
+
+ inline void SpawnServer(ZenServerInstance& Server, ZenConfig& Cfg) { Cfg.Spawn(Server); }
+
+ inline CompressedBuffer CreateSemiRandomBlob(size_t AttachmentSize,
+ OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast)
+ {
+ // Convoluted way to get a compressed buffer whose result it large enough to be a separate file
+ // but also does actually compress
+ const size_t PartCount = (AttachmentSize / (1u * 1024u * 64)) + 1;
+ const size_t PartSize = AttachmentSize / PartCount;
+ auto Part = SharedBuffer(CreateRandomBlob(PartSize));
+ std::vector<SharedBuffer> Parts(PartCount, Part);
+ size_t RemainPartSize = AttachmentSize - (PartSize * PartCount);
+ if (RemainPartSize > 0)
+ {
+ Parts.push_back(SharedBuffer(CreateRandomBlob(RemainPartSize)));
+ }
+ CompressedBuffer Value = CompressedBuffer::Compress(CompositeBuffer(std::move(Parts)), OodleCompressor::Mermaid, CompressionLevel);
+ return Value;
+ };
+
+ inline std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments(const std::span<const size_t>& Sizes)
+ {
+ std::vector<std::pair<Oid, CompressedBuffer>> Result;
+ Result.reserve(Sizes.size());
+ for (size_t Size : Sizes)
+ {
+ CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(CreateRandomBlob(Size)));
+ Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed));
+ }
+ return Result;
+ }
+
+ inline std::vector<std::pair<Oid, CompressedBuffer>> CreateSemiRandomAttachments(const std::span<const size_t>& Sizes)
+ {
+ std::vector<std::pair<Oid, CompressedBuffer>> Result;
+ Result.reserve(Sizes.size());
+ for (size_t Size : Sizes)
+ {
+ CompressedBuffer Compressed =
+ CreateSemiRandomBlob(Size, Size > 1024u * 1024u ? OodleCompressionLevel::None : OodleCompressionLevel::VeryFast);
+ Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed));
+ }
+ return Result;
+ }
+
+} // namespace utils
+
+class ZenServerTestHelper
+{
+public:
+ ZenServerTestHelper(std::string_view HelperId, int ServerCount) : m_HelperId{HelperId}, m_ServerCount{ServerCount} {}
+ ~ZenServerTestHelper() {}
+
+ void SpawnServers(std::string_view AdditionalServerArgs = std::string_view())
+ {
+ SpawnServers([](ZenServerInstance&) {}, AdditionalServerArgs);
+ }
+
+ void SpawnServers(auto&& Callback, std::string_view AdditionalServerArgs)
+ {
+ ZEN_INFO("{}: spawning {} server instances", m_HelperId, m_ServerCount);
+
+ m_Instances.resize(m_ServerCount);
+
+ for (int i = 0; i < m_ServerCount; ++i)
+ {
+ auto& Instance = m_Instances[i];
+ Instance = std::make_unique<ZenServerInstance>(TestEnv);
+ Instance->SetTestDir(TestEnv.CreateNewTestDir());
+ }
+
+ for (int i = 0; i < m_ServerCount; ++i)
+ {
+ auto& Instance = m_Instances[i];
+ Callback(*Instance);
+ }
+
+ for (int i = 0; i < m_ServerCount; ++i)
+ {
+ auto& Instance = m_Instances[i];
+ Instance->SpawnServer(TestEnv.GetNewPortNumber(), AdditionalServerArgs);
+ }
+
+ for (int i = 0; i < m_ServerCount; ++i)
+ {
+ auto& Instance = m_Instances[i];
+ uint16_t PortNumber = Instance->WaitUntilReady();
+ CHECK_MESSAGE(PortNumber != 0, Instance->GetLogOutput());
+ }
+ }
+
+ ZenServerInstance& GetInstance(int Index) { return *m_Instances[Index]; }
+
+private:
+ std::string m_HelperId;
+ int m_ServerCount = 0;
+ std::vector<std::unique_ptr<ZenServerInstance>> m_Instances;
+};
+
+inline std::string
+OidAsString(const Oid& Id)
+{
+ StringBuilder<25> OidStringBuilder;
+ Id.ToString(OidStringBuilder);
+ return OidStringBuilder.ToString();
+}
+
+} // namespace zen::tests
+
+#endif