diff options
| author | Stefan Boberg <[email protected]> | 2026-04-11 13:37:19 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-04-11 13:37:19 +0200 |
| commit | b481ba4cb40e8c8e1781d1fa74b2fc5c89564a0f (patch) | |
| tree | 21860bcdc05665e2a9b2ef50b911156cbc0fe4db /src/zenserver-test/projectstore-tests.cpp | |
| parent | Separate action and worker chunk stores for compute service (diff) | |
| parent | hub deprovision all (#938) (diff) | |
| download | zen-sb/memory-cid-store.tar.xz zen-sb/memory-cid-store.zip | |
Merge branch 'main' into sb/memory-cid-storesb/memory-cid-store
Diffstat (limited to 'src/zenserver-test/projectstore-tests.cpp')
| -rw-r--r-- | src/zenserver-test/projectstore-tests.cpp | 1090 |
1 files changed, 530 insertions, 560 deletions
diff --git a/src/zenserver-test/projectstore-tests.cpp b/src/zenserver-test/projectstore-tests.cpp index cec453511..49d985abb 100644 --- a/src/zenserver-test/projectstore-tests.cpp +++ b/src/zenserver-test/projectstore-tests.cpp @@ -41,423 +41,430 @@ TEST_CASE("project.basic") const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); - std::mt19937_64 mt; - - zen::StringBuilder<64> BaseUri; - BaseUri << fmt::format("http://localhost:{}", PortNumber); + std::string ServerUri = 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") + auto CreateProjectAndOplog = [&](std::string_view ProjectName, std::string_view OplogName) -> std::string { + HttpClient Http{ServerUri}; + + zen::CbObjectWriter Body; + Body << "id" << ProjectName; + Body << "root" << RootPath.c_str(); + Body << "project" + << "/zooom"; + Body << "engine" + << "/zooom"; + IoBuffer BodyBuf = Body.Save().GetBuffer().AsIoBuffer(); + auto Response = Http.Post(fmt::format("/prj/{}", ProjectName), BodyBuf); + REQUIRE(Response.StatusCode == HttpResponseCode::Created); + + std::string OplogUri = fmt::format("{}/prj/{}/oplog/{}", ServerUri, ProjectName, OplogName); + HttpClient OplogHttp{OplogUri}; + auto OplogResponse = OplogHttp.Post(""sv, IoBuffer{}, ZenContentType::kCbObject); + REQUIRE(OplogResponse.StatusCode == HttpResponseCode::Created); + + return OplogUri; + }; + + // Create a file at a path exceeding Windows MAX_PATH (260 chars) for long filename testing + std::filesystem::path LongPathDir = RootPath / "longpathtest"; + for (int I = 0; I < 5; ++I) { - { - HttpClient Http{BaseUri}; + LongPathDir /= std::string(50, char('a' + I)); + } + std::filesystem::path LongFilePath = LongPathDir / "testfile.bin"; + std::filesystem::path LongRelPath = LongFilePath.lexically_relative(RootPath); - { - 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); - } + const uint8_t LongPathFileData[] = {0xDE, 0xAD, 0xBE, 0xEF}; + CreateDirectories(MakeSafeAbsolutePath(LongPathDir)); + WriteFile(MakeSafeAbsolutePath(LongFilePath), IoBufferBuilder::MakeCloneFromMemory(LongPathFileData, sizeof(LongPathFileData))); + CHECK(LongRelPath.string().length() > 260); - { - auto Response = Http.Get("/prj/test"sv); - REQUIRE(Response.StatusCode == HttpResponseCode::OK); + std::string LongClientPath = "/{engine}/client"; + for (int I = 0; I < 5; ++I) + { + LongClientPath += '/'; + LongClientPath.append(50, char('a' + I)); + } + LongClientPath += "/longfile.bin"; + CHECK(LongClientPath.length() > 260); - CbObject ResponseObject = Response.AsObject(); + const std::string_view LongPathChunkId{ + "00000000" + "00000000" + "00020000"}; + auto LongPathFileOid = zen::Oid::FromHexString(LongPathChunkId); - CHECK(ResponseObject["id"].AsString() == "test"sv); - CHECK(ResponseObject["root"].AsString() == PathToUtf8(RootPath.c_str())); - } + // --- build store persistence --- + // First section also verifies project and oplog creation responses. + { + HttpClient ServerHttp{ServerUri}; + + { + zen::CbObjectWriter Body; + Body << "id" + << "test_persist"; + Body << "root" << RootPath.c_str(); + Body << "project" + << "/zooom"; + Body << "engine" + << "/zooom"; + IoBuffer BodyBuf = Body.Save().GetBuffer().AsIoBuffer(); + + auto Response = ServerHttp.Post("/prj/test_persist"sv, BodyBuf); + CHECK(Response.StatusCode == HttpResponseCode::Created); } - BaseUri << "/prj/test/oplog/foobar"; + { + auto Response = ServerHttp.Get("/prj/test_persist"sv); + REQUIRE(Response.StatusCode == HttpResponseCode::OK); + + CbObject ResponseObject = Response.AsObject(); + + CHECK(ResponseObject["id"].AsString() == "test_persist"sv); + CHECK(ResponseObject["root"].AsString() == PathToUtf8(RootPath.c_str())); + } + + std::string OplogUri = fmt::format("{}/prj/test_persist/oplog/oplog_persist", ServerUri); { - HttpClient Http{BaseUri}; + HttpClient OplogHttp{OplogUri}; { - auto Response = Http.Post(""sv, IoBuffer{}, ZenContentType::kCbObject); + auto Response = OplogHttp.Post(""sv, IoBuffer{}, ZenContentType::kCbObject); CHECK(Response.StatusCode == HttpResponseCode::Created); } { - auto Response = Http.Get(""sv); + auto Response = OplogHttp.Get(""sv); REQUIRE(Response.StatusCode == HttpResponseCode::OK); CbObject ResponseObject = Response.AsObject(); - CHECK(ResponseObject["id"].AsString() == "foobar"sv); - CHECK(ResponseObject["project"].AsString() == "test"sv); + CHECK(ResponseObject["id"].AsString() == "oplog_persist"sv); + CHECK(ResponseObject["project"].AsString() == "test_persist"sv); } } - // Create a file at a path exceeding Windows MAX_PATH (260 chars) for long filename testing - std::filesystem::path LongPathDir = RootPath / "longpathtest"; - for (int I = 0; I < 5; ++I) - { - LongPathDir /= std::string(50, char('a' + I)); - } - std::filesystem::path LongFilePath = LongPathDir / "testfile.bin"; - std::filesystem::path LongRelPath = LongFilePath.lexically_relative(RootPath); + uint8_t AttachData[] = {1, 2, 3}; - const uint8_t LongPathFileData[] = {0xDE, 0xAD, 0xBE, 0xEF}; - CreateDirectories(MakeSafeAbsolutePath(LongPathDir)); - WriteFile(MakeSafeAbsolutePath(LongFilePath), IoBufferBuilder::MakeCloneFromMemory(LongPathFileData, sizeof(LongPathFileData))); - CHECK(LongRelPath.string().length() > 260); + zen::CompressedBuffer Attachment = zen::CompressedBuffer::Compress(zen::SharedBuffer::Clone(zen::MemoryView{AttachData, 3})); + zen::CbAttachment Attach{Attachment, Attachment.DecodeRawHash()}; - std::string LongClientPath = "/{engine}/client"; - for (int I = 0; I < 5; ++I) - { - LongClientPath += '/'; - LongClientPath.append(50, char('a' + I)); - } - LongClientPath += "/longfile.bin"; - CHECK(LongClientPath.length() > 260); + zen::CbObjectWriter OpWriter; + OpWriter << "key" + << "foo" + << "attachment" << Attach; - const std::string_view LongPathChunkId{ + const std::string_view ChunkId{ "00000000" "00000000" - "00020000"}; - auto LongPathFileOid = zen::Oid::FromHexString(LongPathChunkId); + "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.BeginObject(); + OpWriter << "id" << LongPathFileOid; + OpWriter << "clientpath" << LongClientPath; + OpWriter << "serverpath" << LongRelPath.c_str(); + OpWriter.EndObject(); + OpWriter.EndArray(); + + zen::CbObject Op = OpWriter.Save(); + + zen::CbPackage OpPackage(Op); + OpPackage.AddAttachment(Attach); - 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.BeginObject(); - OpWriter << "id" << LongPathFileOid; - OpWriter << "clientpath" << LongClientPath; - OpWriter << "serverpath" << LongRelPath.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())); + zen::BinaryWriter MemOut; + legacy::SaveCbPackage(OpPackage, MemOut); - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::Created); - } + HttpClient Http{OplogUri}; - // Read file data + { + auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView())); - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << ChunkId; - auto Response = Http.Get(ChunkGetUri); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::Created); + } - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::OK); - } + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << ChunkId; + auto Response = Http.Get(ChunkGetUri); - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << ChunkId << "?offset=1&size=10"; - auto Response = Http.Get(ChunkGetUri); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + } - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::OK); - CHECK(Response.ResponsePayload.GetSize() == 10); - } + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << ChunkId << "?offset=1&size=10"; + auto Response = Http.Get(ChunkGetUri); - // Read long-path file data - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << LongPathChunkId; - auto Response = Http.Get(ChunkGetUri); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + CHECK(Response.ResponsePayload.GetSize() == 10); + } - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::OK); - CHECK(Response.ResponsePayload.GetSize() == sizeof(LongPathFileData)); - } + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << LongPathChunkId; + auto Response = Http.Get(ChunkGetUri); - ZEN_INFO("+++++++"); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + CHECK(Response.ResponsePayload.GetSize() == sizeof(LongPathFileData)); } - 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.BeginObject(); - OpWriter << "id" << LongPathFileOid; - OpWriter << "clientpath" << LongClientPath; - OpWriter << "serverpath" << LongRelPath.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}; + ZEN_INFO("+++++++"); + } - { - auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView())); + // --- snapshot --- + { + std::string OplogUri = CreateProjectAndOplog("test_snap", "oplog_snap"); - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::Created); - } + zen::CbObjectWriter OpWriter; + OpWriter << "key" + << "foo"; - // Read file data, it is raw and uncompressed - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << ChunkId; - auto Response = Http.Get(ChunkGetUri); + 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.BeginObject(); + OpWriter << "id" << LongPathFileOid; + OpWriter << "clientpath" << LongClientPath; + OpWriter << "serverpath" << LongRelPath.c_str(); + OpWriter.EndObject(); + OpWriter.EndArray(); + + zen::CbObject Op = OpWriter.Save(); + + zen::CbPackage OpPackage(Op); - REQUIRE(Response); - REQUIRE(Response.StatusCode == HttpResponseCode::OK); + zen::BinaryWriter MemOut; + legacy::SaveCbPackage(OpPackage, MemOut); - IoBuffer Data = Response.ResponsePayload; - IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath); - CHECK(ReferenceData.GetSize() == Data.GetSize()); - CHECK(ReferenceData.GetView().EqualBytes(Data.GetView())); - } + HttpClient Http{OplogUri}; - // Read long-path file data, it is raw and uncompressed - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << LongPathChunkId; - auto Response = Http.Get(ChunkGetUri); + { + auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView())); - REQUIRE(Response); - REQUIRE(Response.StatusCode == HttpResponseCode::OK); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::Created); + } - IoBuffer Data = Response.ResponsePayload; - MemoryView ExpectedView{LongPathFileData, sizeof(LongPathFileData)}; - CHECK(Data.GetSize() == sizeof(LongPathFileData)); - CHECK(Data.GetView().EqualBytes(ExpectedView)); - } + // Read file data, it is raw and uncompressed + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << ChunkId; + auto Response = Http.Get(ChunkGetUri); - { - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); - auto Response = Http.Post("/rpc"sv, Payload); - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::OK); - } + REQUIRE(Response); + REQUIRE(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"}}); + IoBuffer Data = Response.ResponsePayload; + IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath); + CHECK(ReferenceData.GetSize() == Data.GetSize()); + CHECK(ReferenceData.GetView().EqualBytes(Data.GetView())); + } - REQUIRE(Response); - REQUIRE(Response.StatusCode == HttpResponseCode::OK); + // Read long-path file data, it is raw and uncompressed + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << LongPathChunkId; + auto Response = Http.Get(ChunkGetUri); - IoBuffer Data = Response.ResponsePayload; - IoHash RawHash; - uint64_t RawSize; - CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); - REQUIRE(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())); - } + REQUIRE(Response); + REQUIRE(Response.StatusCode == HttpResponseCode::OK); - // Read compressed long-path file data after snapshot - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << LongPathChunkId; - auto Response = Http.Get(ChunkGetUri, {{"Accept-Type", "application/x-ue-comp"}}); + IoBuffer Data = Response.ResponsePayload; + MemoryView ExpectedView{LongPathFileData, sizeof(LongPathFileData)}; + CHECK(Data.GetSize() == sizeof(LongPathFileData)); + CHECK(Data.GetView().EqualBytes(ExpectedView)); + } - REQUIRE(Response); - REQUIRE(Response.StatusCode == HttpResponseCode::OK); + { + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); + auto Response = Http.Post("/rpc"sv, Payload); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + } - IoBuffer Data = Response.ResponsePayload; - IoHash RawHash; - uint64_t RawSize; - CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); - REQUIRE(Compressed); - IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer(); - MemoryView ExpectedView{LongPathFileData, sizeof(LongPathFileData)}; - CHECK(RawSize == sizeof(LongPathFileData)); - CHECK(DataDecompressed.GetSize() == sizeof(LongPathFileData)); - CHECK(DataDecompressed.GetView().EqualBytes(ExpectedView)); - } + // 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); + REQUIRE(Response.StatusCode == HttpResponseCode::OK); - ZEN_INFO("+++++++"); + IoBuffer Data = Response.ResponsePayload; + IoHash RawHash; + uint64_t RawSize; + CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); + REQUIRE(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())); } - SUBCASE("snapshot zero byte file") + // Read compressed long-path file data after snapshot { - // A zero-byte file referenced in an oplog entry must survive a - // snapshot: the file is read, compressed, stored in CidStore, and - // the oplog is rewritten with a BinaryAttachment reference. After - // the snapshot the chunk must be retrievable and decompress to an - // empty payload. - - std::filesystem::path EmptyFileRelPath = std::filesystem::path("zerobyte_snapshot_test") / "empty.bin"; - std::filesystem::path EmptyFileAbsPath = RootPath / EmptyFileRelPath; - CreateDirectories(MakeSafeAbsolutePath(EmptyFileAbsPath.parent_path())); - // Create a zero-byte file on disk. - WriteFile(MakeSafeAbsolutePath(EmptyFileAbsPath), IoBuffer{}); - REQUIRE(IsFile(MakeSafeAbsolutePath(EmptyFileAbsPath))); - - const std::string_view EmptyChunkId{ - "00000000" - "00000000" - "00030000"}; - auto EmptyFileOid = zen::Oid::FromHexString(EmptyChunkId); - - zen::CbObjectWriter OpWriter; - OpWriter << "key" - << "zero_byte_test"; - OpWriter.BeginArray("files"); - OpWriter.BeginObject(); - OpWriter << "id" << EmptyFileOid; - OpWriter << "clientpath" - << "/{engine}/empty_file"; - OpWriter << "serverpath" << EmptyFileRelPath.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}; + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << LongPathChunkId; + auto Response = Http.Get(ChunkGetUri, {{"Accept-Type", "application/x-ue-comp"}}); - { - auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView())); - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::Created); - } + REQUIRE(Response); + REQUIRE(Response.StatusCode == HttpResponseCode::OK); - // Read file data before snapshot - raw and uncompressed, 0 bytes. - // http.sys converts a 200 OK with empty body to 204 No Content, so - // accept either status code. - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << EmptyChunkId; - auto Response = Http.Get(ChunkGetUri); + IoBuffer Data = Response.ResponsePayload; + IoHash RawHash; + uint64_t RawSize; + CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); + REQUIRE(Compressed); + IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer(); + MemoryView ExpectedView{LongPathFileData, sizeof(LongPathFileData)}; + CHECK(RawSize == sizeof(LongPathFileData)); + CHECK(DataDecompressed.GetSize() == sizeof(LongPathFileData)); + CHECK(DataDecompressed.GetView().EqualBytes(ExpectedView)); + } - REQUIRE(Response); - CHECK((Response.StatusCode == HttpResponseCode::OK || Response.StatusCode == HttpResponseCode::NoContent)); - CHECK(Response.ResponsePayload.GetSize() == 0); - } + ZEN_INFO("+++++++"); + } - // Trigger snapshot. - { - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); - auto Response = Http.Post("/rpc"sv, Payload); - REQUIRE(Response); - CHECK(Response.StatusCode == HttpResponseCode::OK); - } + // --- snapshot zero byte file --- + { + std::string OplogUri = CreateProjectAndOplog("test_zero", "oplog_zero"); - // Read chunk after snapshot - compressed, decompresses to 0 bytes. - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << "/" << EmptyChunkId; - auto Response = Http.Get(ChunkGetUri, {{"Accept-Type", "application/x-ue-comp"}}); + std::filesystem::path EmptyFileRelPath = std::filesystem::path("zerobyte_snapshot_test") / "empty.bin"; + std::filesystem::path EmptyFileAbsPath = RootPath / EmptyFileRelPath; + CreateDirectories(MakeSafeAbsolutePath(EmptyFileAbsPath.parent_path())); + WriteFile(MakeSafeAbsolutePath(EmptyFileAbsPath), IoBuffer{}); + REQUIRE(IsFile(MakeSafeAbsolutePath(EmptyFileAbsPath))); - REQUIRE(Response); - REQUIRE(Response.StatusCode == HttpResponseCode::OK); + const std::string_view EmptyChunkId{ + "00000000" + "00000000" + "00030000"}; + auto EmptyFileOid = zen::Oid::FromHexString(EmptyChunkId); + + zen::CbObjectWriter OpWriter; + OpWriter << "key" + << "zero_byte_test"; + OpWriter.BeginArray("files"); + OpWriter.BeginObject(); + OpWriter << "id" << EmptyFileOid; + OpWriter << "clientpath" + << "/{engine}/empty_file"; + OpWriter << "serverpath" << EmptyFileRelPath.c_str(); + OpWriter.EndObject(); + OpWriter.EndArray(); + + zen::CbObject Op = OpWriter.Save(); + zen::CbPackage OpPackage(Op); - IoBuffer Data = Response.ResponsePayload; - IoHash RawHash; - uint64_t RawSize; - CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); - REQUIRE(Compressed); - CHECK(RawSize == 0); - IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer(); - CHECK(DataDecompressed.GetSize() == 0); - } + zen::BinaryWriter MemOut; + legacy::SaveCbPackage(OpPackage, MemOut); - // Cleanup - { - std::error_code Ec; - DeleteDirectories(MakeSafeAbsolutePath(RootPath / "zerobyte_snapshot_test"), Ec); - } + HttpClient Http{OplogUri}; - ZEN_INFO("+++++++"); + { + auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView())); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::Created); } - SUBCASE("test chunk not found error") + // Read file data before snapshot - raw and uncompressed, 0 bytes. + // http.sys converts a 200 OK with empty body to 204 No Content, so + // accept either status code. { - HttpClient Http{BaseUri}; + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << EmptyChunkId; + auto Response = Http.Get(ChunkGetUri); - for (size_t I = 0; I < 65; I++) - { - zen::StringBuilder<128> PostUri; - PostUri << "/f77c781846caead318084604/info"; - auto Response = Http.Get(PostUri); + REQUIRE(Response); + CHECK((Response.StatusCode == HttpResponseCode::OK || Response.StatusCode == HttpResponseCode::NoContent)); + CHECK(Response.ResponsePayload.GetSize() == 0); + } - REQUIRE(!Response.Error); - CHECK(Response.StatusCode == HttpResponseCode::NotFound); - } + // Trigger snapshot. + { + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); + auto Response = Http.Post("/rpc"sv, Payload); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + } + + // Read chunk after snapshot - compressed, decompresses to 0 bytes. + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << EmptyChunkId; + auto Response = Http.Get(ChunkGetUri, {{"Accept-Type", "application/x-ue-comp"}}); + + REQUIRE(Response); + REQUIRE(Response.StatusCode == HttpResponseCode::OK); + + IoBuffer Data = Response.ResponsePayload; + IoHash RawHash; + uint64_t RawSize; + CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); + REQUIRE(Compressed); + CHECK(RawSize == 0); + IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer(); + CHECK(DataDecompressed.GetSize() == 0); } - // Cleanup long-path test directory { std::error_code Ec; - DeleteDirectories(MakeSafeAbsolutePath(RootPath / "longpathtest"), Ec); + DeleteDirectories(MakeSafeAbsolutePath(RootPath / "zerobyte_snapshot_test"), Ec); + } + + ZEN_INFO("+++++++"); + } + + // --- test chunk not found error --- + { + std::string OplogUri = CreateProjectAndOplog("test_notfound", "oplog_notfound"); + HttpClient Http{OplogUri}; + + 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); } } + + // Cleanup long-path test directory + { + std::error_code Ec; + DeleteDirectories(MakeSafeAbsolutePath(RootPath / "longpathtest"), Ec); + } } CbPackage @@ -753,86 +760,102 @@ TEST_CASE("project.remote") } }; - SUBCASE("File") + // --- Zen --- + // NOTE: Zen export must run before file-based exports from the same source + // oplog. A prior file export leaves server-side state that causes a + // subsequent zen-protocol export from the same oplog to abort. { + INFO("Zen"); ScopedTemporaryDirectory TempDir; { - IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) { + std::string ExportSourceUri = Servers.GetInstance(0).GetBaseUri(); + std::string ExportTargetUri = Servers.GetInstance(1).GetBaseUri(); + MakeProject(ExportTargetUri, "proj0_zen"); + MakeOplog(ExportTargetUri, "proj0_zen", "oplog0_zen"); + + 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 << "maxchunksperblock"sv << 16u; + Writer << "chunkfilesizelimit"sv << 5u * 1024u; Writer << "force"sv << false; - Writer << "file"sv << BeginObject; + Writer << "zen"sv << BeginObject; { - Writer << "path"sv << path; - Writer << "name"sv - << "proj0_oplog0"sv; + Writer << "url"sv << ExportTargetUri.substr(7); + Writer << "project" + << "proj0_zen"; + Writer << "oplog" + << "oplog0_zen"; } - Writer << EndObject; // "file" + Writer << EndObject; // "zen" } Writer << EndObject; // "params" }); - HttpClient Http{Servers.GetInstance(0).GetBaseUri()}; - + 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_zen", "oplog0_zen"); + ValidateOplog(1, "proj0_zen", "oplog0_zen"); + { - MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy"); - MakeOplog(Servers.GetInstance(1).GetBaseUri(), "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([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) { + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer << "method"sv << "import"sv; Writer << "params" << BeginObject; { Writer << "force"sv << false; - Writer << "file"sv << BeginObject; + Writer << "zen"sv << BeginObject; { - Writer << "path"sv << path; - Writer << "name"sv - << "proj0_oplog0"sv; + Writer << "url"sv << ImportSourceUri.substr(7); + Writer << "project" + << "proj0_zen"; + Writer << "oplog" + << "oplog0_zen"; } - Writer << EndObject; // "file" + Writer << EndObject; // "zen" } 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); + 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(1, "proj0_copy", "oplog0_copy"); - ValidateOplog(1, "proj0_copy", "oplog0_copy"); + ValidateAttachments(2, "proj1", "oplog1"); + ValidateOplog(2, "proj1", "oplog1"); } - SUBCASE("File disable blocks") + // --- File --- { + INFO("File"); ScopedTemporaryDirectory TempDir; { - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { + 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 << "maxchunksperblock"sv << 16u; Writer << "chunkfilesizelimit"sv << 5u * 1024u; - Writer << "force"sv << false; + Writer << "maxchunksperblock"sv << 16u; + Writer << "force"sv << true; Writer << "file"sv << BeginObject; { - Writer << "path"sv << TempDir.Path().string(); + Writer << "path"sv << path; Writer << "name"sv << "proj0_oplog0"sv; - Writer << "disableblocks"sv << true; } Writer << EndObject; // "file" } @@ -845,9 +868,10 @@ TEST_CASE("project.remote") 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) { + MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_file"); + MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_file", "oplog0_file"); + + IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) { Writer << "method"sv << "import"sv; Writer << "params" << BeginObject; @@ -855,7 +879,7 @@ TEST_CASE("project.remote") Writer << "force"sv << false; Writer << "file"sv << BeginObject; { - Writer << "path"sv << TempDir.Path().string(); + Writer << "path"sv << path; Writer << "name"sv << "proj0_oplog0"sv; } @@ -866,15 +890,16 @@ TEST_CASE("project.remote") HttpClient Http{Servers.GetInstance(1).GetBaseUri()}; - HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload); + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_file", "oplog0_file"), Payload); HttpWaitForCompletion(Servers.GetInstance(1), Response); } - ValidateAttachments(1, "proj0_copy", "oplog0_copy"); - ValidateOplog(1, "proj0_copy", "oplog0_copy"); + ValidateAttachments(1, "proj0_file", "oplog0_file"); + ValidateOplog(1, "proj0_file", "oplog0_file"); } - SUBCASE("File force temp blocks") + // --- File disable blocks --- { + INFO("File disable blocks"); ScopedTemporaryDirectory TempDir; { IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { @@ -886,26 +911,27 @@ TEST_CASE("project.remote") Writer << "maxchunkembedsize"sv << 1296u; Writer << "maxchunksperblock"sv << 16u; Writer << "chunkfilesizelimit"sv << 5u * 1024u; - Writer << "force"sv << false; + Writer << "force"sv << true; Writer << "file"sv << BeginObject; { Writer << "path"sv << TempDir.Path().string(); Writer << "name"sv << "proj0_oplog0"sv; - Writer << "enabletempblocks"sv << true; + Writer << "disableblocks"sv << true; } Writer << EndObject; // "file" } Writer << EndObject; // "params" }); - HttpClient Http{Servers.GetInstance(0).GetBaseUri()}; + 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"); + MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_noblock"); + MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_noblock", "oplog0_noblock"); IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer << "method"sv << "import"sv; @@ -923,23 +949,20 @@ TEST_CASE("project.remote") Writer << EndObject; // "params" }); - HttpClient Http{Servers.GetInstance(1).GetBaseUri()}; - HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload); + HttpClient Http{Servers.GetInstance(1).GetBaseUri()}; + + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_noblock", "oplog0_noblock"), Payload); HttpWaitForCompletion(Servers.GetInstance(1), Response); } - ValidateAttachments(1, "proj0_copy", "oplog0_copy"); - ValidateOplog(1, "proj0_copy", "oplog0_copy"); + ValidateAttachments(1, "proj0_noblock", "oplog0_noblock"); + ValidateOplog(1, "proj0_noblock", "oplog0_noblock"); } - SUBCASE("Zen") + // --- File force temp blocks --- { + INFO("File force temp blocks"); 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; @@ -949,14 +972,13 @@ TEST_CASE("project.remote") Writer << "maxchunkembedsize"sv << 1296u; Writer << "maxchunksperblock"sv << 16u; Writer << "chunkfilesizelimit"sv << 5u * 1024u; - Writer << "force"sv << false; - Writer << "zen"sv << BeginObject; + Writer << "force"sv << true; + Writer << "file"sv << BeginObject; { - Writer << "url"sv << ExportTargetUri.substr(7); - Writer << "project" - << "proj0_copy"; - Writer << "oplog" - << "oplog0_copy"; + Writer << "path"sv << TempDir.Path().string(); + Writer << "name"sv + << "proj0_oplog0"sv; + Writer << "enabletempblocks"sv << true; } Writer << EndObject; // "file" } @@ -967,40 +989,32 @@ TEST_CASE("project.remote") 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"); - + MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_tmpblock"); + MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_tmpblock", "oplog0_tmpblock"); IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer << "method"sv << "import"sv; Writer << "params" << BeginObject; { Writer << "force"sv << false; - Writer << "zen"sv << BeginObject; + Writer << "file"sv << BeginObject; { - Writer << "url"sv << ImportSourceUri.substr(7); - Writer << "project" - << "proj0_copy"; - Writer << "oplog" - << "oplog0_copy"; + Writer << "path"sv << TempDir.Path().string(); + Writer << "name"sv + << "proj0_oplog0"sv; } 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); + HttpClient Http{Servers.GetInstance(1).GetBaseUri()}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_tmpblock", "oplog0_tmpblock"), Payload); + HttpWaitForCompletion(Servers.GetInstance(1), Response); } - ValidateAttachments(2, "proj1", "oplog1"); - ValidateOplog(2, "proj1", "oplog1"); + ValidateAttachments(1, "proj0_tmpblock", "oplog0_tmpblock"); + ValidateOplog(1, "proj0_tmpblock", "oplog0_tmpblock"); } } @@ -1379,7 +1393,7 @@ TEST_CASE("project.file.data.transitions") return Package; }; - SUBCASE("path-referenced file is retrievable") + // --- path-referenced file is retrievable --- { MakeProject("proj_path"sv); MakeOplog("proj_path"sv, "oplog"sv); @@ -1397,7 +1411,7 @@ TEST_CASE("project.file.data.transitions") } } - SUBCASE("hash-referenced file is retrievable") + // --- hash-referenced file is retrievable --- { MakeProject("proj_hash"sv); MakeOplog("proj_hash"sv, "oplog"sv); @@ -1416,34 +1430,35 @@ TEST_CASE("project.file.data.transitions") } } - SUBCASE("hash-referenced to path-referenced transition with different content") + struct TransitionVariant { - MakeProject("proj_hash_to_path_diff"sv); - MakeOplog("proj_hash_to_path_diff"sv, "oplog"sv); + std::string_view Suffix; + bool SameOpKey; + bool RunGc; + }; - Oid FirstOpKey = Oid::NewOid(); - Oid SecondOpKey; - bool RunGcAfterTransition = false; + static constexpr TransitionVariant Variants[] = { + {"_nk", false, false}, + {"_sk", true, false}, + {"_nk_gc", false, true}, + {"_sk_gc", true, true}, + }; - SUBCASE("new op key") { SecondOpKey = Oid::NewOid(); } - SUBCASE("same op key") { SecondOpKey = FirstOpKey; } - SUBCASE("new op key with gc") - { - SecondOpKey = Oid::NewOid(); - RunGcAfterTransition = true; - } - SUBCASE("same op key with gc") - { - SecondOpKey = FirstOpKey; - RunGcAfterTransition = true; - } + // --- hash-referenced to path-referenced transition with different content --- + for (const TransitionVariant& V : Variants) + { + std::string ProjName = fmt::format("proj_h2pd{}", V.Suffix); + MakeProject(ProjName); + MakeOplog(ProjName, "oplog"sv); + + Oid FirstOpKey = Oid::NewOid(); + Oid SecondOpKey = V.SameOpKey ? FirstOpKey : Oid::NewOid(); - // First op: file with CAS hash (content differs from the on-disk file) { CbPackage Op = BuildHashReferencedFileOp(FirstOpKey, CompressedBlob); - PostOplogEntry("proj_hash_to_path_diff"sv, "oplog"sv, Op); + PostOplogEntry(ProjName, "oplog"sv, Op); - HttpClient::Response Response = GetChunk("proj_hash_to_path_diff"sv); + HttpClient::Response Response = GetChunk(ProjName); CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk first op")); if (Response.IsSuccess()) { @@ -1453,19 +1468,17 @@ TEST_CASE("project.file.data.transitions") } } - // Second op: same FileId transitions to serverpath (different data) { CbPackage Op = BuildPathReferencedFileOp(SecondOpKey); - PostOplogEntry("proj_hash_to_path_diff"sv, "oplog"sv, Op); + PostOplogEntry(ProjName, "oplog"sv, Op); } - if (RunGcAfterTransition) + if (V.RunGc) { TriggerGcAndWait(); } - // Must serve the on-disk file content, not the old CAS blob - HttpClient::Response Response = GetChunk("proj_hash_to_path_diff"sv); + HttpClient::Response Response = GetChunk(ProjName); CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk after transition")); if (Response.IsSuccess()) { @@ -1475,95 +1488,68 @@ TEST_CASE("project.file.data.transitions") } } - SUBCASE("hash-referenced to path-referenced transition with identical content") + // --- hash-referenced to path-referenced transition with identical content --- { - // Compress the same on-disk file content as a CAS blob so both references yield identical data CompressedBuffer MatchingBlob = CompressedBuffer::Compress(SharedBuffer::Clone(FileBlob.GetView())); - MakeProject("proj_hash_to_path_same"sv); - MakeOplog("proj_hash_to_path_same"sv, "oplog"sv); + for (const TransitionVariant& V : Variants) + { + std::string ProjName = fmt::format("proj_h2ps{}", V.Suffix); + MakeProject(ProjName); + MakeOplog(ProjName, "oplog"sv); - Oid FirstOpKey = Oid::NewOid(); - Oid SecondOpKey; - bool RunGcAfterTransition = false; + Oid FirstOpKey = Oid::NewOid(); + Oid SecondOpKey = V.SameOpKey ? FirstOpKey : Oid::NewOid(); - SUBCASE("new op key") { SecondOpKey = Oid::NewOid(); } - SUBCASE("same op key") { SecondOpKey = FirstOpKey; } - SUBCASE("new op key with gc") - { - SecondOpKey = Oid::NewOid(); - RunGcAfterTransition = true; - } - SUBCASE("same op key with gc") - { - SecondOpKey = FirstOpKey; - RunGcAfterTransition = true; - } + { + CbPackage Op = BuildHashReferencedFileOp(FirstOpKey, MatchingBlob); + PostOplogEntry(ProjName, "oplog"sv, Op); - // First op: file with CAS hash (content matches the on-disk file) - { - CbPackage Op = BuildHashReferencedFileOp(FirstOpKey, MatchingBlob); - PostOplogEntry("proj_hash_to_path_same"sv, "oplog"sv, Op); + HttpClient::Response Response = GetChunk(ProjName); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk first op")); + if (Response.IsSuccess()) + { + IoBuffer Payload = GetDecompressedPayload(Response); + CHECK(Payload.GetView().EqualBytes(FileBlob.GetView())); + } + } - HttpClient::Response Response = GetChunk("proj_hash_to_path_same"sv); - CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk first op")); + { + CbPackage Op = BuildPathReferencedFileOp(SecondOpKey); + PostOplogEntry(ProjName, "oplog"sv, Op); + } + + if (V.RunGc) + { + TriggerGcAndWait(); + } + + HttpClient::Response Response = GetChunk(ProjName); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk after transition")); if (Response.IsSuccess()) { IoBuffer Payload = GetDecompressedPayload(Response); + CHECK_EQ(Payload.GetSize(), FileBlob.GetSize()); CHECK(Payload.GetView().EqualBytes(FileBlob.GetView())); } } - - // Second op: same FileId transitions to serverpath (same data) - { - CbPackage Op = BuildPathReferencedFileOp(SecondOpKey); - PostOplogEntry("proj_hash_to_path_same"sv, "oplog"sv, Op); - } - - if (RunGcAfterTransition) - { - TriggerGcAndWait(); - } - - // Must still resolve successfully after the transition - HttpClient::Response Response = GetChunk("proj_hash_to_path_same"sv); - CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk after transition")); - if (Response.IsSuccess()) - { - IoBuffer Payload = GetDecompressedPayload(Response); - CHECK_EQ(Payload.GetSize(), FileBlob.GetSize()); - CHECK(Payload.GetView().EqualBytes(FileBlob.GetView())); - } } - SUBCASE("path-referenced to hash-referenced transition with different content") + // --- path-referenced to hash-referenced transition with different content --- + for (const TransitionVariant& V : Variants) { - MakeProject("proj_path_to_hash_diff"sv); - MakeOplog("proj_path_to_hash_diff"sv, "oplog"sv); - - Oid FirstOpKey = Oid::NewOid(); - Oid SecondOpKey; - bool RunGcAfterTransition = false; + std::string ProjName = fmt::format("proj_p2hd{}", V.Suffix); + MakeProject(ProjName); + MakeOplog(ProjName, "oplog"sv); - SUBCASE("new op key") { SecondOpKey = Oid::NewOid(); } - SUBCASE("same op key") { SecondOpKey = FirstOpKey; } - SUBCASE("new op key with gc") - { - SecondOpKey = Oid::NewOid(); - RunGcAfterTransition = true; - } - SUBCASE("same op key with gc") - { - SecondOpKey = FirstOpKey; - RunGcAfterTransition = true; - } + Oid FirstOpKey = Oid::NewOid(); + Oid SecondOpKey = V.SameOpKey ? FirstOpKey : Oid::NewOid(); - // First op: file with serverpath { CbPackage Op = BuildPathReferencedFileOp(FirstOpKey); - PostOplogEntry("proj_path_to_hash_diff"sv, "oplog"sv, Op); + PostOplogEntry(ProjName, "oplog"sv, Op); - HttpClient::Response Response = GetChunk("proj_path_to_hash_diff"sv); + HttpClient::Response Response = GetChunk(ProjName); CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk first op")); if (Response.IsSuccess()) { @@ -1572,19 +1558,17 @@ TEST_CASE("project.file.data.transitions") } } - // Second op: same FileId transitions to CAS hash (different data) { CbPackage Op = BuildHashReferencedFileOp(SecondOpKey, CompressedBlob); - PostOplogEntry("proj_path_to_hash_diff"sv, "oplog"sv, Op); + PostOplogEntry(ProjName, "oplog"sv, Op); } - if (RunGcAfterTransition) + if (V.RunGc) { TriggerGcAndWait(); } - // Must serve the CAS blob content, not the old on-disk file - HttpClient::Response Response = GetChunk("proj_path_to_hash_diff"sv); + HttpClient::Response Response = GetChunk(ProjName); CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk after transition")); if (Response.IsSuccess()) { @@ -1595,65 +1579,51 @@ TEST_CASE("project.file.data.transitions") } } - SUBCASE("path-referenced to hash-referenced transition with identical content") + // --- path-referenced to hash-referenced transition with identical content --- { - // Compress the same on-disk file content as a CAS blob so both references yield identical data CompressedBuffer MatchingBlob = CompressedBuffer::Compress(SharedBuffer::Clone(FileBlob.GetView())); - MakeProject("proj_path_to_hash_same"sv); - MakeOplog("proj_path_to_hash_same"sv, "oplog"sv); + for (const TransitionVariant& V : Variants) + { + std::string ProjName = fmt::format("proj_p2hs{}", V.Suffix); + MakeProject(ProjName); + MakeOplog(ProjName, "oplog"sv); - Oid FirstOpKey = Oid::NewOid(); - Oid SecondOpKey; - bool RunGcAfterTransition = false; + Oid FirstOpKey = Oid::NewOid(); + Oid SecondOpKey = V.SameOpKey ? FirstOpKey : Oid::NewOid(); - SUBCASE("new op key") { SecondOpKey = Oid::NewOid(); } - SUBCASE("same op key") { SecondOpKey = FirstOpKey; } - SUBCASE("new op key with gc") - { - SecondOpKey = Oid::NewOid(); - RunGcAfterTransition = true; - } - SUBCASE("same op key with gc") - { - SecondOpKey = FirstOpKey; - RunGcAfterTransition = true; - } + { + CbPackage Op = BuildPathReferencedFileOp(FirstOpKey); + PostOplogEntry(ProjName, "oplog"sv, Op); - // First op: file with serverpath - { - CbPackage Op = BuildPathReferencedFileOp(FirstOpKey); - PostOplogEntry("proj_path_to_hash_same"sv, "oplog"sv, Op); + HttpClient::Response Response = GetChunk(ProjName); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk first op")); + if (Response.IsSuccess()) + { + IoBuffer Payload = GetDecompressedPayload(Response); + CHECK(Payload.GetView().EqualBytes(FileBlob.GetView())); + } + } - HttpClient::Response Response = GetChunk("proj_path_to_hash_same"sv); - CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk first op")); + { + CbPackage Op = BuildHashReferencedFileOp(SecondOpKey, MatchingBlob); + PostOplogEntry(ProjName, "oplog"sv, Op); + } + + if (V.RunGc) + { + TriggerGcAndWait(); + } + + HttpClient::Response Response = GetChunk(ProjName); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk after transition")); if (Response.IsSuccess()) { IoBuffer Payload = GetDecompressedPayload(Response); + CHECK_EQ(Payload.GetSize(), FileBlob.GetSize()); CHECK(Payload.GetView().EqualBytes(FileBlob.GetView())); } } - - // Second op: same FileId transitions to CAS hash (same data) - { - CbPackage Op = BuildHashReferencedFileOp(SecondOpKey, MatchingBlob); - PostOplogEntry("proj_path_to_hash_same"sv, "oplog"sv, Op); - } - - if (RunGcAfterTransition) - { - TriggerGcAndWait(); - } - - // Must still resolve successfully after the transition - HttpClient::Response Response = GetChunk("proj_path_to_hash_same"sv); - CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("GetChunk after transition")); - if (Response.IsSuccess()) - { - IoBuffer Payload = GetDecompressedPayload(Response); - CHECK_EQ(Payload.GetSize(), FileBlob.GetSize()); - CHECK(Payload.GetView().EqualBytes(FileBlob.GetView())); - } } } |