Files
cdc-file-transfer/manifest/manifest_builder_test.cc
T
Christian Schneider 4326e972ac Releasing the former Stadia file transfer tools
The tools allow efficient and fast synchronization of large directory
trees from a Windows workstation to a Linux target machine.

cdc_rsync* support efficient copy of files by using content-defined
chunking (CDC) to identify chunks within files that can be reused.

asset_stream_manager + cdc_fuse_fs support efficient streaming of a
local directory to a remote virtual file system based on FUSE. It also
employs CDC to identify and reuse unchanged data chunks.
2022-11-03 10:39:10 +01:00

1139 lines
40 KiB
C++

// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "manifest/manifest_builder.h"
#include "absl/time/clock.h"
#include "common/path.h"
#include "common/status_test_macros.h"
#include "common/util.h"
#include "data_store/mem_data_store.h"
#include "gtest/gtest.h"
#include "manifest/content_id.h"
#include "manifest/manifest_iterator.h"
#include "manifest/manifest_printer.h"
namespace cdc_ft {
namespace {
// Helper function that tries to parse data as any of the protos written to the
// store and returns its text proto representation.
//
// In order to disambiguate the proto auto-detection logic, you can temporarily
// assign globally unique field numbers to all fields in manifest.proto.
std::string ToTextProto(const ContentIdProto& content_id, const void* data,
size_t size) {
std::string text_proto;
std::string proto_name = "(unknown proto format)";
ManifestProto manifest_pb;
AssetListProto asset_list_pb;
ChunkListProto chunk_list_pb;
int isize = static_cast<int>(size);
ManifestPrinter printer;
if (manifest_pb.ParseFromArray(data, isize) &&
!manifest_pb.GetReflection()
->GetUnknownFields(manifest_pb)
.field_count()) {
printer.PrintToString(manifest_pb, &text_proto);
proto_name = manifest_pb.GetTypeName();
} else if (asset_list_pb.ParseFromArray(data, isize) &&
!asset_list_pb.GetReflection()
->GetUnknownFields(asset_list_pb)
.field_count()) {
printer.PrintToString(asset_list_pb, &text_proto);
proto_name = asset_list_pb.GetTypeName();
} else if (chunk_list_pb.ParseFromArray(data, isize) &&
!chunk_list_pb.GetReflection()
->GetUnknownFields(chunk_list_pb)
.field_count()) {
printer.PrintToString(chunk_list_pb, &text_proto);
proto_name = chunk_list_pb.GetTypeName();
}
return absl::StrFormat("# %s => %s (size: %d)\n%s",
ContentId::ToHexString(content_id), proto_name, isize,
text_proto);
}
class ManifestBuilderTest : public ::testing::Test {
public:
ManifestBuilderTest() = default;
void SetUp() override {
ASSERT_OK(cache_.Wipe());
content_ids_.clear();
expected_assets_.clear();
// 4KiB
cdc_params_.set_avg_chunk_size(4u << 10);
}
void TearDown() override {}
protected:
// A list of strings.
using StringList = std::vector<std::string>;
// A set of strings.
using StringSet = std::unordered_set<std::string>;
using TypeStringsMap = std::unordered_map<AssetProto::Type, StringSet>;
// Maps a file path to a list of chunks representing the file contents.
using AssetMap = std::unordered_map<std::string, StringList>;
// Maps a chunk by its content to the content ID.
using ContentIdMap = std::unordered_map<std::string, ContentIdProto>;
// Identifes a symlink if this is the first chunk of a file.
static constexpr char kSymlinkPrefix[] = "->";
// The size of adding one reference to an IndirectAssetList to a DIRECTORY
// asset.
static constexpr size_t kIndirectAssetListRefSize = 24;
// The overhead for adding a variable-size field (like another proto) to a
// proto message.
static constexpr size_t kProtoFieldOverhead = 2;
// Returns the maximum proto size that we expect to encounter. This is
// slightly higher than the avg_chunk_size due to some overhead bytes which
// are not accounted for, for the sake of simplicity:
// - Since the size is only enforced on the directory level and the manifest
// root node includes the root directory asset and the CdcParamsProto, the
// latter can cause the limit to be slightly exceeded.
// - When the first asset is pushed to an indirect asset list, the
// indirectlist reference also creates an overhead that is not accounted for.
size_t ActualMaxProtoSize() const {
return cdc_params_.avg_chunk_size() + kProtoFieldOverhead +
cdc_params_.ByteSizeLong() + kIndirectAssetListRefSize;
}
// Adds a list of pseudo assets to the |builder|. The AssetMap maps file names
// to a list of chunks (just strings). If the chunk list is empty, it's
// considered to be a directory, otherwise a file. To represent an empty file,
// add a single empty string as a chunk.
absl::Status AddAssets(const AssetMap& assets, ManifestBuilder* builder) {
for (auto [path, chunks] : assets) {
AssetProto::Type type = GetAssetType(chunks);
AssetBuilder ab;
// Add the asset.
ASSIGN_OR_RETURN(ab, builder->GetOrCreateAsset(path, type));
switch (type) {
case AssetProto::DIRECTORY:
break;
case AssetProto::FILE:
// Add the chunks, if any.
for (std::string chunk : chunks) {
if (chunk.empty()) continue;
ContentIdProto content_id = ContentId::FromDataString(chunk);
ab.AppendChunk(content_id, chunk.size());
content_ids_[chunk] = content_id;
}
break;
case AssetProto::SYMLINK:
ab.SetSymlinkTarget(chunks.back());
break;
default:
return absl::InternalError(absl::StrFormat(
"Unhandled asset type: %s", AssetProto::Type_Name(type)));
}
// Add the asset to the set of expected assets.
std::string unix_path = path::ToUnix(path);
expected_assets_[type].insert(unix_path);
// Add all directories leading up to this asset.
std::vector<absl::string_view> parts = SplitString(unix_path, '/', false);
for (size_t i = 1; i < parts.size(); ++i) {
expected_assets_[AssetProto::DIRECTORY].insert(
JoinStrings(parts, 0, i, '/'));
}
}
return absl::OkStatus();
}
// Verifies that the manifest identified by |manifest_id| correctly and
// completely describes the given |assets| map. In particular, it verifies
// that:
// - all assets and intermediate directories exist
// - no extra assets or directories exist
// - FILE assets have the correct chunks w/ offsets
// - FILE assets have the correct size
// - SYMLINK assets point to the right target
void VerifyAssets(const AssetMap& assets, const ContentIdProto& manifest_id) {
std::unordered_map<AssetProto::Type, StringSet> found;
// Verify the manifest by iterating over it.
ManifestIterator iter(&cache_);
ASSERT_OK(iter.Open(manifest_id));
const AssetProto* asset;
while ((asset = iter.NextEntry()) != nullptr) {
std::string assetPath =
path::JoinUnix(path::ToUnix(iter.RelativePath()), asset->name());
// Evaluate expected file size.
StringList chunks;
if (auto it = assets.find(assetPath); it != assets.end()) {
chunks = it->second;
} else {
// For windows paths, we need to search through all assets.
for (auto [name, asset_chunks] : assets) {
if (path::ToUnix(name) == assetPath) {
chunks = asset_chunks;
break;
}
}
}
size_t file_size = GetFileSize(chunks);
AssetProto::Type type = GetAssetType(chunks);
EXPECT_EQ(type, asset->type());
found[type].insert(assetPath);
// Verify type specific proto fields.
switch (type) {
case AssetProto::DIRECTORY:
break;
case AssetProto::FILE:
EXPECT_EQ(asset->file_size(), file_size);
VerifyChunks(chunks, asset);
break;
case AssetProto::SYMLINK:
EXPECT_EQ(chunks.size(), 2);
EXPECT_EQ(asset->symlink_target(), chunks.back());
break;
default:
ASSERT_TRUE(false)
<< "Unexpected asset type: " << AssetProto::Type_Name(type);
}
}
// Make sure there was no error processing the manifest.
EXPECT_OK(iter.Status());
// Make sure all expected assets were found.
EXPECT_EQ(found, expected_assets_);
}
// Verifies that the given |file| proto consists of the chunks given as
// |chunks|.
void VerifyChunks(const StringList& expected_chunks, const AssetProto* file) {
uint64_t offset = 0;
// Verify the direct chunks.
VerifyChunkList(expected_chunks, file->file_chunks(), 0, &offset);
// Verify all indirect chunks.
int chunk_index = file->file_chunks_size();
for (const IndirectChunkListProto& indirect_chunks :
file->file_indirect_chunks()) {
EXPECT_EQ(offset, indirect_chunks.offset());
ASSERT_GT(indirect_chunks.chunk_list_id().blake3_sum_160().size(), 0);
ChunkListProto chunks;
ASSERT_OK(cache_.GetProto(indirect_chunks.chunk_list_id(), &chunks));
VerifyChunkList(expected_chunks, chunks.chunks(), chunk_index, &offset);
chunk_index += chunks.chunks_size();
}
// Make sure we found all the chunks.
if (file->file_size() > 0) EXPECT_EQ(expected_chunks.size(), chunk_index);
}
void VerifyChunkList(const StringList& expected_chunks,
const RepeatedChunkRefProto& actual_chunks,
int chunk_index, size_t* offset) {
ASSERT_LE(chunk_index + actual_chunks.size(), expected_chunks.size());
uint64_t relative_offset = 0;
for (const ChunkRefProto& chunk_ref : actual_chunks) {
const std::string& chunk = expected_chunks[chunk_index++];
ContentIdProto expected_id = content_ids_.at(chunk);
ASSERT_GT(expected_id.blake3_sum_160().size(), 0);
ASSERT_EQ(expected_id, chunk_ref.chunk_id());
EXPECT_EQ(relative_offset, chunk_ref.offset());
relative_offset += chunk.size();
}
*offset += relative_offset;
}
AssetProto::Type GetAssetType(const StringList& chunks) const {
// Asset with an empty chunk list: DIRECTORY
if (chunks.empty()) return AssetProto::DIRECTORY;
// Asset with a single chunk starting with kSymlinkPrefix: SYMLINK
if (chunks.size() == 2 && chunks[0] == kSymlinkPrefix) {
return AssetProto::SYMLINK;
}
// Otherwise: FILE
return AssetProto::FILE;
}
size_t GetFileSize(const StringList& chunks) const {
size_t file_size = 0;
for (const std::string& chunk : chunks) {
file_size += chunk.size();
}
return file_size;
}
// Iterates over the manifest identifed by |manifest_id| and returns the list
// of assets. Does not return the relative asset path.
std::vector<AssetProto> GetAllAssets(const ContentIdProto& manifest_id) {
// Verify the manifest by iterating over it.
ManifestIterator iter(&cache_);
EXPECT_OK(iter.Open(manifest_id));
std::vector<AssetProto> assets;
const AssetProto* asset;
while ((asset = iter.NextEntry()) != nullptr) {
assets.push_back(*asset);
}
EXPECT_OK(iter.Status());
return assets;
}
// Default CDC parameters.
CdcParamsProto cdc_params_;
// Maps chunks back to content IDs.
ContentIdMap content_ids_;
// List of expected assets.
TypeStringsMap expected_assets_;
MemDataStore cache_;
};
TEST_F(ManifestBuilderTest, SimpleFiles) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a", "b"};
assets["file2.txt"] = {"c", "d"};
assets["file3.txt"] = {"e"};
assets["file4.txt"] = {""};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, FilesDirsCreatedOnlyOnce) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a"};
assets["dir1"] = {};
bool created = false;
EXPECT_OK(
builder.GetOrCreateAsset("file1.txt", AssetProto::FILE, false, &created));
EXPECT_TRUE(created);
EXPECT_OK(
builder.GetOrCreateAsset("file1.txt", AssetProto::FILE, false, &created));
EXPECT_FALSE(created);
EXPECT_OK(
builder.GetOrCreateAsset("dir1", AssetProto::DIRECTORY, false, &created));
EXPECT_TRUE(created);
EXPECT_OK(
builder.GetOrCreateAsset("dir1", AssetProto::DIRECTORY, false, &created));
EXPECT_FALSE(created);
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, Deduplication) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a"};
assets["file2.txt"] = {"a", "a"};
assets["file3.txt"] = {"a", "a", "a"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, TruncateChunks) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a", "b"};
assets["file2.txt"] = {"c", "d"};
assets["file3.txt"] = {""};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
absl::StatusOr<AssetBuilder> file2 =
builder.GetOrCreateAsset("file2.txt", AssetProto::FILE);
ASSERT_OK(file2);
file2->TruncateChunks();
ASSERT_OK(builder.Flush());
assets["file2.txt"] = {""};
VerifyAssets(assets, builder.ManifestId());
absl::StatusOr<AssetBuilder> file3 =
builder.GetOrCreateAsset("file3.txt", AssetProto::FILE);
ASSERT_OK(file3);
file3->TruncateChunks();
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, SimpleDirs) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["dir1"] = {};
assets["dir2"] = {};
assets["dir2/dir3"] = {};
assets["dir2/dir3/dir4"] = {};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, DirsAreCreated) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a", "b"};
assets["dir1/file1.txt"] = {"c", "d"};
assets["dir1/file2.txt"] = {""};
assets["dir1/dir2/file3.txt"] = {"e"};
assets["dir3/dir4/dir5"] = {};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, DirsWithWindowsPathSep) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["dir1\\file1.txt"] = {"a", "b"};
assets["dir1\\file2.txt"] = {""};
assets["dir1\\dir2\\file3.txt"] = {"c"};
assets["dir3\\dir4\\dir5"] = {};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, SomeDirsAreCreated) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["dir1"] = {};
assets["dir1/file1.txt"] = {"a"};
assets["dir1/dir2/file2.txt"] = {"b"};
assets["dir1/dir2/dir3"] = {};
assets["dir4/dir5/dir6"] = {};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, Symlinks) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["dir1"] = {};
assets["dir1/file1.txt"] = {"a"};
assets["file2.txt"] = {"b"};
assets["link1"] = {kSymlinkPrefix, "dir1"};
assets["link2"] = {kSymlinkPrefix, "dir1/file1.txt"};
assets["link3"] = {kSymlinkPrefix, "file2.txt"};
assets["link4"] = {kSymlinkPrefix, "does_not_exist"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, Spaces) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["dir 1"] = {};
assets["dir 1/file 1.txt"] = {"a"};
assets["this is file2.txt"] = {"b"};
assets["link with spaces"] = {kSymlinkPrefix, "dir 1/file 1.txt"};
assets["a path/to some/file with spaces.txt"] = {"c", "d"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, FilePermissions) {
ManifestBuilder builder(cdc_params_, &cache_);
std::unordered_map<std::string, uint32_t> expected_perms;
expected_perms["file_0777.txt"] = 0777u;
expected_perms["file_0755.txt"] = 0755u;
expected_perms["file_0644.txt"] = 0644u;
expected_perms["file_0664.txt"] = 0664u;
expected_perms["file_0444.txt"] = 0444u;
expected_perms["file_0640.txt"] = 0640u;
for (auto [name, perms] : expected_perms) {
absl::StatusOr<AssetBuilder> ab =
builder.GetOrCreateAsset(name, AssetProto::FILE);
ASSERT_OK(ab);
ab->SetPermissions(perms);
}
ASSERT_OK(builder.Flush());
std::vector<AssetProto> assets = GetAllAssets(builder.ManifestId());
EXPECT_EQ(assets.size(), expected_perms.size());
for (const AssetProto& asset : assets) {
EXPECT_EQ(asset.permissions(), expected_perms[asset.name()]);
}
}
TEST_F(ManifestBuilderTest, DirPermissions) {
ManifestBuilder builder(cdc_params_, &cache_);
std::unordered_map<std::string, uint32_t> expected_perms;
expected_perms["dir_0777"] = 0777u;
expected_perms["dir_0755"] = 0755u;
expected_perms["dir_0750"] = 0750u;
expected_perms["dir_0700"] = 0700u;
for (auto [name, perms] : expected_perms) {
absl::StatusOr<AssetBuilder> ab =
builder.GetOrCreateAsset(name, AssetProto::DIRECTORY);
ASSERT_OK(ab);
ab->SetPermissions(perms);
}
ASSERT_OK(builder.Flush());
std::vector<AssetProto> assets = GetAllAssets(builder.ManifestId());
EXPECT_EQ(assets.size(), expected_perms.size());
for (const AssetProto& asset : assets) {
EXPECT_EQ(asset.permissions(), expected_perms[asset.name()]);
}
}
TEST_F(ManifestBuilderTest, DefaultPermissions) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["dir1"] = {};
assets["dir1/file1.txt"] = {"a"};
assets["file2.txt"] = {"b"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
std::vector<AssetProto> asset_pbs = GetAllAssets(builder.ManifestId());
for (const AssetProto& asset : asset_pbs) {
if (asset.type() == AssetProto::DIRECTORY) {
EXPECT_EQ(asset.permissions(), ManifestBuilder::kDefaultDirPerms);
} else {
EXPECT_EQ(asset.permissions(), ManifestBuilder::kDefaultFilePerms);
}
}
}
TEST_F(ManifestBuilderTest, FileMTime) {
ManifestBuilder builder(cdc_params_, &cache_);
std::unordered_map<std::string, int64_t> expected_mtime;
expected_mtime["file1.txt"] = 0;
expected_mtime["file2.txt"] = 42;
expected_mtime["file3.txt"] = absl::ToUnixSeconds(absl::Now());
// A negative time value 42 days before the Unix epoch.
expected_mtime["file4.txt"] =
absl::ToUnixSeconds(absl::UnixEpoch() - absl::Hours(24 * 42));
for (auto [name, mtime] : expected_mtime) {
absl::StatusOr<AssetBuilder> ab =
builder.GetOrCreateAsset(name, AssetProto::FILE);
ASSERT_OK(ab);
ab->SetMtimeSeconds(mtime);
}
ASSERT_OK(builder.Flush());
std::vector<AssetProto> assets = GetAllAssets(builder.ManifestId());
EXPECT_EQ(assets.size(), expected_mtime.size());
for (const AssetProto& asset : assets) {
EXPECT_EQ(asset.mtime_seconds(), expected_mtime[asset.name()]);
}
}
TEST_F(ManifestBuilderTest, DirMTime) {
ManifestBuilder builder(cdc_params_, &cache_);
std::unordered_map<std::string, int64_t> expected_mtime;
expected_mtime["dir1"] = 0;
expected_mtime["dir2"] = 42;
expected_mtime["dir3"] = absl::ToUnixSeconds(absl::Now());
// A negative time value 42 days before the Unix epoch.
expected_mtime["dir4"] =
absl::ToUnixSeconds(absl::UnixEpoch() - absl::Hours(24 * 42));
// Auto-create "dir2" first with a file to see if the mtime is correctly
// overridden.
absl::StatusOr<AssetBuilder> ab =
builder.GetOrCreateAsset("dir1/file1.txt", AssetProto::FILE);
ASSERT_OK(ab);
for (auto [name, mtime] : expected_mtime) {
ab = builder.GetOrCreateAsset(name, AssetProto::DIRECTORY);
ASSERT_OK(ab);
ab->SetMtimeSeconds(mtime);
}
ASSERT_OK(builder.Flush());
std::vector<AssetProto> assets = GetAllAssets(builder.ManifestId());
EXPECT_EQ(assets.size(), expected_mtime.size() + 1); // +1 file
for (const AssetProto& asset : assets) {
if (asset.type() == AssetProto::FILE) {
// The file's timestamp must be current.
EXPECT_LE(asset.mtime_seconds(), absl::ToUnixSeconds(absl::Now()));
EXPECT_GE(asset.mtime_seconds() + 60, absl::ToUnixSeconds(absl::Now()));
} else {
EXPECT_EQ(asset.mtime_seconds(), expected_mtime[asset.name()])
<< "Asset: " << asset.name();
}
}
}
TEST_F(ManifestBuilderTest, IndirectAssets) {
// A proto with just the type and name set is serialized to 4 + sizeof(name)
// bytes.
//
// FILE assets:
// - Each ChunkRef proto asset with a small offset (<128) takes up 28 bytes.
// - With 0 chunks: 4 + sizeof(name)
// - With 1 chunk: 32 + sizeof(name)
// - With 2 chunks: 60 + sizeof(name)
//
// DIRECTORY assets:
// - Each IndirectChunkList proto asset with a small offset (<128) takes up 28
// bytes (same as for a ChunkRef in a file).
//
// ChunkList protos:
// - Again, each ChunkRef proto adds 28 bytes.
//
// AssetList proto:
// - Each Asset in the list has an overhead of 2 additional bytes.
cdc_params_.set_avg_chunk_size(128);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
// 61 bytes each, so one AssetList of 128 bytes can fit 2 assets.
assets["a"] = {"a", "a"};
assets["b"] = {"b", "b"};
assets["c"] = {"c", "c"};
assets["d"] = {"d", "d"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Expecting 1 manifest proto, 1 AssetList proto, 4 ChunkList protos.
EXPECT_EQ(cache_.Chunks().size(), 6);
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
// Verify that the chunk size is not exeeded.
size_t max_proto_size = ActualMaxProtoSize();
for (const auto& [content_id, chunk] : cache_.Chunks()) {
EXPECT_LE(chunk.size(), max_proto_size)
<< ToTextProto(content_id, chunk.data(), chunk.size());
}
}
TEST_F(ManifestBuilderTest, IndirectChunks) {
cdc_params_.set_avg_chunk_size(128);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
// Each chunk adds 28 bytes so one IndirectChunkList of 128 bytes can fit 4
// chunks (= 112 bytes).
assets["a"] = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Expecting 1 manifest proto and 3 ChunkList protos.
EXPECT_EQ(cache_.Chunks().size(), 4);
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
// Verify that the chunk size is not exeeded.
size_t max_proto_size = ActualMaxProtoSize();
for (const auto& [content_id, chunk] : cache_.Chunks()) {
EXPECT_LE(chunk.size(), max_proto_size)
<< ToTextProto(content_id, chunk.data(), chunk.size());
}
}
TEST_F(ManifestBuilderTest, IndirectChunksFlushedTwice) {
cdc_params_.set_avg_chunk_size(256);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets, more_assets;
// Each chunk adds 28 bytes so one IndirectChunkList of 256 bytes can fit 9
// chunks (= 252 bytes). This asset will still be considered large even after
// it was flushed.
assets["a"] = {"a", "b", "c", "d", "e", "f", "g", "h"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
// Add more FILEs to the root directory such that it exceeds the chunk size
// limit. The next Flush() call will also try to push chunks for FILE "a" to
// indirect lists again, which must be ignored.
more_assets["b"] = {"i"};
more_assets["c"] = {"i"};
more_assets["d"] = {"i"};
more_assets["e"] = {"i"};
more_assets["f"] = {"i"};
ASSERT_OK(AddAssets(more_assets, &builder));
ASSERT_OK(builder.Flush());
assets.insert(more_assets.begin(), more_assets.end());
VerifyAssets(assets, builder.ManifestId());
// Expecting 2 manifest protos (old and new one), 1 ChunkList proto, and 1
// AssetList proto.
EXPECT_EQ(cache_.Chunks().size(), 4);
// 1 new manifest, 1 new ChunkList, 1 updated AssetList.
EXPECT_EQ(builder.FlushedContentIds().size(), 3);
// Verify that the chunk size is not exeeded.
size_t max_proto_size = ActualMaxProtoSize();
for (const auto& [content_id, chunk] : cache_.Chunks()) {
EXPECT_LE(chunk.size(), max_proto_size)
<< ToTextProto(content_id, chunk.data(), chunk.size());
}
}
TEST_F(ManifestBuilderTest, IndirectChunksAndAssets) {
cdc_params_.set_avg_chunk_size(128);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
// Each chunk adds 28 bytes so one IndirectChunkList of 128 bytes can fit 4
// chunks (= 112 bytes).
assets["a"] = {"a", "b", "c", "d"};
assets["b"] = {"b", "c", "d", "e"};
assets["c"] = {"c", "d", "e", "f"};
assets["d"] = {"d", "e", "f", "g"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Expecting 1 Manifest proto, 1 AssetList proto, 4 ChunkList protos.
EXPECT_EQ(cache_.Chunks().size(), 6);
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
// Verify that the chunk size is not exeeded.
size_t max_proto_size = ActualMaxProtoSize();
for (const auto& [content_id, chunk] : cache_.Chunks()) {
EXPECT_LE(chunk.size(), max_proto_size)
<< ToTextProto(content_id, chunk.data(), chunk.size());
}
}
TEST_F(ManifestBuilderTest, DirectAndIndirectChunks) {
// In order to have both direct and indirect chunks, we need a minimum "large
// asset" size of 64 bytes, which translates to a chunk size of 64 << 4 = 1024
// bytes.
cdc_params_.set_avg_chunk_size(1024);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
// Each chunk adds 30 bytes each, so one IndirectChunkList of
// 1024 bytes can fit 34 chunks (= 1020 bytes).
assets["a"] = {"00", "01", "02", "03", "04", "05", "06", "07", "08",
"09", "10", "11", "12", "13", "14", "15", "16", "17",
"18", "19", "20", "21", "22", "23", "24", "25", "26",
"27", "28", "29", "30", "31", "32", "33", "34", "35"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Expecting 1 manifest proto and 1 ChunkList protos.
EXPECT_EQ(cache_.Chunks().size(), 2);
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
// Verify that the chunk size is not exeeded.
size_t max_proto_size = ActualMaxProtoSize();
for (const auto& [content_id, chunk] : cache_.Chunks()) {
EXPECT_LE(chunk.size(), max_proto_size)
<< ToTextProto(content_id, chunk.data(), chunk.size());
}
}
TEST_F(ManifestBuilderTest, SetChunksWithIndirectChunks) {
cdc_params_.set_avg_chunk_size(128);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
// Each chunk adds 28 bytes so one IndirectChunkList of 128 bytes can fit 4
// chunks (= 112 bytes).
assets["a"] = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Expecting 1 manifest proto and 3 ChunkList protos.
EXPECT_EQ(cache_.Chunks().size(), 4);
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
// Create a new chunk list consisting of a single chunk.
RepeatedChunkRefProto chunks;
ChunkRefProto* chunk = chunks.Add();
assets["a"] = {"new chunk data"};
*chunk->mutable_chunk_id() = ContentId::FromDataString(assets["a"][0]);
content_ids_[assets["a"][0]] = chunk->chunk_id();
// Set the single chunk list.
auto asset = builder.GetOrCreateAsset("a", AssetProto::FILE);
ASSERT_OK(asset);
asset->SetChunks(chunks, assets["a"][0].size());
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Only the manifest was updated.
EXPECT_EQ(builder.FlushedContentIds().size(), 1);
// Set to an empty chunk list.
asset = builder.GetOrCreateAsset("a", AssetProto::FILE);
ASSERT_OK(asset);
asset->SetChunks(RepeatedChunkRefProto(), 0);
assets["a"] = {""};
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Only the manifest was updated.
EXPECT_EQ(builder.FlushedContentIds().size(), 1);
}
TEST_F(ManifestBuilderTest, SwapChunksWithIndirectChunks) {
cdc_params_.set_avg_chunk_size(128);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
// Each chunk adds 28 bytes so one IndirectChunkList of 128 bytes can fit 4
// chunks (= 112 bytes).
assets["a"] = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Expecting 1 manifest proto and 3 ChunkList protos.
EXPECT_EQ(cache_.Chunks().size(), 4);
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
// Create a new chunk list consisting of a single chunk.
RepeatedChunkRefProto chunks;
ChunkRefProto* chunk = chunks.Add();
assets["a"] = {"new chunk data"};
*chunk->mutable_chunk_id() = ContentId::FromDataString(assets["a"][0]);
content_ids_[assets["a"][0]] = chunk->chunk_id();
// Swap with the single chunk list.
auto asset = builder.GetOrCreateAsset("a", AssetProto::FILE);
ASSERT_OK(asset);
asset->SwapChunks(&chunks, assets["a"][0].size());
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Only the manifest changed.
EXPECT_EQ(builder.FlushedContentIds().size(), 1);
// Swap with an empty chunk list.
chunks.Clear();
asset = builder.GetOrCreateAsset("a", AssetProto::FILE);
ASSERT_OK(asset);
asset->SwapChunks(&chunks, 0);
assets["a"] = {""};
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Only the manifest changed.
EXPECT_EQ(builder.FlushedContentIds().size(), 1);
}
TEST_F(ManifestBuilderTest, DeleteDirectAssets) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a", "b"};
assets["dir1/file1.txt"] = {"c", "d"};
assets["dir1/file2.txt"] = {""};
assets["dir1/dir2/file3.txt"] = {"e"};
assets["dir3/dir4/dir5"] = {};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
EXPECT_OK(builder.DeleteAsset("file1.txt"));
ASSERT_OK(builder.Flush());
expected_assets_[AssetProto::FILE].erase("file1.txt");
VerifyAssets(assets, builder.ManifestId());
EXPECT_OK(builder.DeleteAsset("dir1/file2.txt"));
ASSERT_OK(builder.Flush());
expected_assets_[AssetProto::FILE].erase("dir1/file2.txt");
VerifyAssets(assets, builder.ManifestId());
EXPECT_OK(builder.DeleteAsset("dir1/dir2"));
ASSERT_OK(builder.Flush());
expected_assets_[AssetProto::DIRECTORY].erase("dir1/dir2");
expected_assets_[AssetProto::FILE].erase("dir1/dir2/file3.txt");
VerifyAssets(assets, builder.ManifestId());
EXPECT_OK(builder.DeleteAsset("dir3"));
ASSERT_OK(builder.Flush());
expected_assets_[AssetProto::DIRECTORY].erase("dir3");
expected_assets_[AssetProto::DIRECTORY].erase("dir3/dir4");
expected_assets_[AssetProto::DIRECTORY].erase("dir3/dir4/dir5");
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, DeleteIndirectAssets) {
// A proto with just the type and name set is serialized to 4 + sizeof(name)
// bytes.
//
// FILE assets:
// - Each ChunkRef proto asset with a small offset (<128) takes up 28 bytes.
// - With 0 chunks: 4 + sizeof(name)
//
// DIRECTORY assets:
// - Each IndirectChunkList proto asset with a small offset (<128) takes up 28
// bytes (same as for a ChunkRef in a file).
//
// AssetList proto:
// - Each Asset in the list has an overhead of 2 additional bytes.
cdc_params_.set_avg_chunk_size(64);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
// Each empty file weighs 17 bytes in the list, so one AssetList of 64 bytes
// can fit up to 3 FILE assets.
assets["f1"] = {""};
assets["d1/f2"] = {""};
assets["d1/f3"] = {""};
assets["d1/f4"] = {""};
assets["d1/d2/f5"] = {""};
assets["d1/d2/f6"] = {""};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Expecting 1 manifest proto, 5 AssetList protos.
EXPECT_EQ(cache_.Chunks().size(), 5);
EXPECT_EQ(builder.FlushedContentIds().size(), cache_.Chunks().size());
size_t max_proto_size = ActualMaxProtoSize();
for (const auto& [content_id, chunk] : cache_.Chunks()) {
EXPECT_LE(chunk.size(), max_proto_size)
<< ToTextProto(content_id, chunk.data(), chunk.size());
}
EXPECT_OK(builder.DeleteAsset("f1"));
ASSERT_OK(builder.Flush());
expected_assets_[AssetProto::FILE].erase("f1");
VerifyAssets(assets, builder.ManifestId());
// 1 manifest + at least 1 AssetList
EXPECT_GT(builder.FlushedContentIds().size(), 1);
EXPECT_OK(builder.DeleteAsset("d1/f3"));
ASSERT_OK(builder.Flush());
expected_assets_[AssetProto::FILE].erase("d1/f3");
VerifyAssets(assets, builder.ManifestId());
// 1 manifest + at least 2 AssetLists
EXPECT_GT(builder.FlushedContentIds().size(), 2);
EXPECT_OK(builder.DeleteAsset("d1/d2"));
ASSERT_OK(builder.Flush());
expected_assets_[AssetProto::DIRECTORY].erase("d1/d2");
expected_assets_[AssetProto::FILE].erase("d1/d2/f5");
expected_assets_[AssetProto::FILE].erase("d1/d2/f6");
VerifyAssets(assets, builder.ManifestId());
// 1 manifest + at least 2 AssetLists
EXPECT_GT(builder.FlushedContentIds().size(), 2);
}
TEST_F(ManifestBuilderTest, LoadAndUpdateManifest) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a", "b"};
assets["dir1/file1.txt"] = {"c", "d"};
assets["dir1/file2.txt"] = {""};
assets["dir1/dir2/file3.txt"] = {"e"};
assets["dir3/dir4/dir5"] = {};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
ManifestBuilder builder2(cdc_params_, &cache_);
ASSERT_OK(builder2.LoadManifest(builder.ManifestId()));
EXPECT_OK(builder2.DeleteAsset("file1.txt"));
EXPECT_OK(builder2.DeleteAsset("dir1/dir2"));
ASSERT_OK(builder2.Flush());
expected_assets_[AssetProto::FILE].erase("file1.txt");
expected_assets_[AssetProto::DIRECTORY].erase("dir1/dir2");
expected_assets_[AssetProto::FILE].erase("dir1/dir2/file3.txt");
VerifyAssets(assets, builder2.ManifestId());
}
TEST_F(ManifestBuilderTest, LoadAndUpdateManifestFromString) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["file1.txt"] = {"a", "b"};
assets["dir1/file1.txt"] = {"c", "d"};
assets["dir1/file2.txt"] = {""};
assets["dir1/dir2/file3.txt"] = {"e"};
assets["dir3/dir4/dir5"] = {};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
ManifestBuilder builder2(cdc_params_, &cache_);
ASSERT_OK(
builder2.LoadManifest(ContentId::ToHexString(builder.ManifestId())));
EXPECT_OK(builder2.DeleteAsset("file1.txt"));
EXPECT_OK(builder2.DeleteAsset("dir1/dir2"));
ASSERT_OK(builder2.Flush());
expected_assets_[AssetProto::FILE].erase("file1.txt");
expected_assets_[AssetProto::DIRECTORY].erase("dir1/dir2");
expected_assets_[AssetProto::FILE].erase("dir1/dir2/file3.txt");
VerifyAssets(assets, builder2.ManifestId());
}
TEST_F(ManifestBuilderTest, ForceCreateDirectory) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["a"] = {"a", "b"};
assets["c/d"] = {"c", "d"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// This is supposed to fail since "a" is a file, not a directory.
auto asset = builder.GetOrCreateAsset("a/b", AssetProto::FILE, false);
EXPECT_NOT_OK(asset);
// This is supposed to fail since "c/d" is a file, not a directory.
asset = builder.GetOrCreateAsset("c/d/e", AssetProto::FILE, false);
EXPECT_NOT_OK(asset);
// Force-create a new file, turning "a" into a directory.
asset = builder.GetOrCreateAsset("a/b", AssetProto::FILE, true);
EXPECT_OK(asset);
ASSERT_OK(builder.Flush());
assets["a"] = {};
assets["a/b"] = {""};
expected_assets_[AssetProto::FILE].erase("a");
expected_assets_[AssetProto::DIRECTORY].insert("a");
expected_assets_[AssetProto::FILE].insert("a/b");
VerifyAssets(assets, builder.ManifestId());
// Force-create a new file, turning "c/d" into a directory.
asset = builder.GetOrCreateAsset("c/d/e", AssetProto::FILE, true);
EXPECT_OK(asset);
ASSERT_OK(builder.Flush());
assets["c/d"] = {};
assets["c/d/e"] = {""};
expected_assets_[AssetProto::FILE].erase("c/d");
expected_assets_[AssetProto::DIRECTORY].insert("c/d");
expected_assets_[AssetProto::FILE].insert("c/d/e");
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, ForceCreateFile) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["a/b"] = {"a", "b"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Force-create a new file, deleting "a/b" and turning "a" into a file.
auto asset = builder.GetOrCreateAsset("a", AssetProto::FILE, true);
EXPECT_OK(asset);
ASSERT_OK(builder.Flush());
assets.clear();
assets["a"] = {""};
expected_assets_.clear();
expected_assets_[AssetProto::FILE].insert("a");
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, ForceCreateFileInSubdir) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["a/b/c"] = {"a", "b"};
assets["a/b/d"] = {"c", "d"};
assets["a/e"] = {"c", "d"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
// Force-create a new file, deleting a/b/* and turning "a/b" into a file.
auto asset = builder.GetOrCreateAsset("a/b", AssetProto::FILE, true);
EXPECT_OK(asset);
ASSERT_OK(builder.Flush());
assets.clear();
assets["a/b"] = {""};
assets["a/e"] = {"c", "d"};
expected_assets_[AssetProto::FILE].erase("a/b/c");
expected_assets_[AssetProto::FILE].erase("a/b/d");
expected_assets_[AssetProto::DIRECTORY].erase("a/b");
expected_assets_[AssetProto::FILE].insert("a/b");
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, GetRootDirectory) {
ManifestBuilder builder(cdc_params_, &cache_);
auto root_dir = builder.GetOrCreateAsset("", AssetProto::DIRECTORY);
ASSERT_OK(root_dir);
// Make sure this is the root directory asset.
EXPECT_TRUE(root_dir->Name().empty());
EXPECT_EQ(root_dir->Type(), AssetProto::DIRECTORY);
EXPECT_EQ(root_dir->Proto()->dir_assets_size(), 0);
ASSERT_OK(builder.Flush());
VerifyAssets(AssetMap(), builder.ManifestId());
}
TEST_F(ManifestBuilderTest, Unicode) {
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets[u8"dir ❤️"] = {};
assets[u8"dir ❤️/file 💩.txt"] = {"a", "b"};
assets[u8"dir ❤️/dir 😀/file 😉.txt"] = {"c", "d"};
assets[u8"❤️/💩/😀"] = {"e", "f"};
assets[u8"link ➡️"] = {kSymlinkPrefix, u8"dir ❤️/dir 😀"};
ASSERT_OK(AddAssets(assets, &builder));
ASSERT_OK(builder.Flush());
VerifyAssets(assets, builder.ManifestId());
}
TEST_F(ManifestBuilderTest, GetManifest) {
ManifestBuilder builder(cdc_params_, &cache_);
EXPECT_EQ(builder.Manifest()->cdc_params().SerializeAsString(),
cdc_params_.SerializeAsString());
}
TEST_F(ManifestBuilderTest, CreateFileLookup) {
cdc_params_.set_avg_chunk_size(128);
ManifestBuilder builder(cdc_params_, &cache_);
AssetMap assets;
assets["a"] = {"a", "a"};
assets["b"] = {"b", "b"};
assets["dir/aa"] = {"aa", "aa"};
assets["dir/bb"] = {"bb", "bb"};
ASSERT_OK(AddAssets(assets, &builder));
ManifestBuilder::FileLookupMap lookup = builder.CreateFileLookup();
ASSERT_EQ(lookup.size(), 4);
EXPECT_EQ(lookup["a"]->name(), "a");
EXPECT_EQ(lookup["b"]->name(), "b");
EXPECT_EQ(lookup["dir/aa"]->name(), "aa");
EXPECT_EQ(lookup["dir/bb"]->name(), "bb");
ASSERT_OK(builder.Flush());
// Builder2 won't have the indirect asset lists loaded.
// Assets in indirect lists won't be in the lookup.
ManifestBuilder builder2(cdc_params_, &cache_);
EXPECT_OK(builder2.LoadManifest(builder.ManifestId()));
lookup = builder2.CreateFileLookup();
ASSERT_EQ(lookup.size(), 2);
EXPECT_EQ(lookup["dir/aa"]->name(), "aa");
EXPECT_EQ(lookup["dir/bb"]->name(), "bb");
}
} // namespace
} // namespace cdc_ft