// Copyright 2024 Huawei Cloud Computing Technology Co., Ltd. // // 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 "src/buildtool/storage/large_object_cas.hpp" #include #include #include #include #include #include #include #include #include #include #include "catch2/catch_test_macros.hpp" #include "src/buildtool/common/artifact_digest.hpp" #include "src/buildtool/common/artifact_digest_factory.hpp" #include "src/buildtool/common/protocol_traits.hpp" #include "src/buildtool/crypto/hash_function.hpp" #include "src/buildtool/execution_api/bazel_msg/bazel_msg_factory.hpp" #include "src/buildtool/execution_api/common/ids.hpp" #include "src/buildtool/file_system/file_system_manager.hpp" #include "src/buildtool/file_system/object_type.hpp" #include "src/buildtool/storage/config.hpp" #include "src/buildtool/storage/garbage_collector.hpp" #include "src/buildtool/storage/local_cas.hpp" #include "src/buildtool/storage/storage.hpp" #include "src/utils/cpp/expected.hpp" #include "src/utils/cpp/tmp_dir.hpp" #include "test/utils/hermeticity/test_storage_config.hpp" #include "test/utils/large_objects/large_object_utils.hpp" namespace { namespace LargeTestUtils { template class Blob final { public: static constexpr auto kLargeId = "bl_8Mb"; static constexpr std::uintmax_t kLargeSize = 8UL * 1024 * 1024; static constexpr auto kSmallId = "bl_1kB"; static constexpr std::uintmax_t kSmallSize = 1024; static constexpr auto kEmptyId = "bl_0"; static constexpr std::uintmax_t kEmptySize = 0; [[nodiscard]] static auto Create( LocalCAS const& cas, std::string const& id, std::uintmax_t size) noexcept -> std::optional>; [[nodiscard]] static auto Generate(std::string const& id, std::uintmax_t size) noexcept -> std::optional; }; using File = Blob; class Tree final { public: static constexpr auto kLargeId = "tree_4096"; static constexpr std::uintmax_t kLargeSize = 4096; static constexpr auto kSmallId = "tree_1"; static constexpr std::uintmax_t kSmallSize = 1; static constexpr auto kEmptyId = "tree_0"; static constexpr std::uintmax_t kEmptySize = 0; [[nodiscard]] static auto Create( LocalCAS const& cas, std::string const& id, std::uintmax_t entries_count) noexcept -> std::optional>; [[nodiscard]] static auto Generate(std::string const& id, std::uintmax_t entries_count) noexcept -> std::optional; [[nodiscard]] static auto StoreRaw( LocalCAS const& cas, std::filesystem::path const& directory) noexcept -> std::optional; }; } // namespace LargeTestUtils } // namespace // Test splitting of a small tree. TEST_CASE("LargeObjectCAS: split a small tree", "[storage]") { auto const storage_config = TestStorageConfig::Create(); auto const storage = Storage::Create(&storage_config.Get()); auto const& cas = storage.CAS(); // Create a small tree: using LargeTestUtils::Tree; auto small = Tree::Create(cas, Tree::kSmallId, Tree::kSmallSize); REQUIRE(small); auto const& [digest, path] = *small; // Split must be successful: auto split_pack = cas.SplitTree(digest); REQUIRE(split_pack); // The result must contain one blob digest: CHECK(split_pack->size() == 1); CHECK_FALSE(split_pack->front().IsTree()); } // Test splitting of a large object. The split must be successful and the entry // must be placed to the LargeCAS. The second split of the same object must load // the result from the LargeCAS, no actual split must occur. // The object can be implicitly reconstructed from the LargeCAS. template static void TestLarge(StorageConfig const& storage_config, Storage const& storage) noexcept { SECTION("Large") { static constexpr bool kIsTree = IsTreeObject(kType); static constexpr bool kIsExec = IsExecutableObject(kType); using TestType = std::conditional_t>; auto const& cas = storage.CAS(); // Create a large object: auto object = TestType::Create(cas, TestType::kLargeId, TestType::kLargeSize); CHECK(object); auto const& [digest, path] = *object; // Split the large object: auto pack_1 = kIsTree ? cas.SplitTree(digest) : cas.SplitBlob(digest); CHECK(pack_1); CHECK(pack_1->size() > 1); CHECK(FileSystemManager::RemoveFile(path)); CHECK_FALSE(FileSystemManager::IsFile(path)); SECTION("Split short-circuiting") { // Check the second call loads the entry from the large CAS: auto pack_2 = kIsTree ? cas.SplitTree(digest) : cas.SplitBlob(digest); CHECK(pack_2); CHECK(pack_2->size() == pack_1->size()); // There must be no spliced file: CHECK_FALSE(FileSystemManager::IsFile(path)); } SECTION("Splice") { // Check implicit splice: auto spliced_path = kIsTree ? cas.TreePath(digest) : cas.BlobPath(digest, kIsExec); REQUIRE(spliced_path); // The result must be in the same location: CHECK(*spliced_path == path); } SECTION("Uplinking") { // Increment generation: CHECK(GarbageCollector::TriggerGarbageCollection(storage_config)); // Check implicit splice: auto spliced_path = kIsTree ? cas.TreePath(digest) : cas.BlobPath(digest, kIsExec); REQUIRE(spliced_path); // The result must be spliced to the same location: CHECK(*spliced_path == path); // Check the large entry was uplinked too: // Remove the spliced result: CHECK(FileSystemManager::RemoveFile(path)); CHECK_FALSE(FileSystemManager::IsFile(path)); // Call split with disabled uplinking: auto const youngest_storage = ::Generation::Create(&storage_config); auto pack_3 = kIsTree ? youngest_storage.CAS().SplitTree(digest) : youngest_storage.CAS().SplitBlob(digest); REQUIRE(pack_3); CHECK(pack_3->size() == pack_1->size()); // Check there are no spliced results in all generations: for (std::size_t i = 0; i < storage_config.num_generations; ++i) { auto const storage = ::Generation::Create(&storage_config); auto generation_path = kIsTree ? storage.CAS().TreePath(digest) : storage.CAS().BlobPath(digest, kIsExec); REQUIRE_FALSE(generation_path); } } } } // Test splitting of a small object. The split must be successful, but the entry // must not be placed to the LargeCAS. The result of spliting must contain one // blob. // The object cannot be implicitly reconstructed. template static void TestSmall(Storage const& storage) noexcept { SECTION("Small") { static constexpr bool kIsTree = IsTreeObject(kType); static constexpr bool kIsExec = IsExecutableObject(kType); using TestType = std::conditional_t>; auto const& cas = storage.CAS(); // Create a small object: auto object = TestType::Create(cas, TestType::kSmallId, TestType::kSmallSize); CHECK(object); auto const& [digest, path] = *object; // Split the small object: auto pack_1 = kIsTree ? cas.SplitTree(digest) : cas.SplitBlob(digest); CHECK(pack_1); CHECK(pack_1->size() == 1); CHECK_FALSE(pack_1->front().IsTree()); // Test that there is no large entry in the storage: // To ensure there is no split of the initial object, it is removed: CHECK(FileSystemManager::RemoveFile(path)); CHECK_FALSE(FileSystemManager::IsFile(path)); // The part of a small executable is the same file but without the // execution permission. It must be deleted too. if constexpr (kIsExec) { auto part_path = cas.BlobPath(pack_1->front(), false); CHECK(part_path); CHECK(FileSystemManager::RemoveFile(*part_path)); } // Split must not find the large entry: auto pack_2 = kIsTree ? cas.SplitTree(digest) : cas.SplitBlob(digest); CHECK_FALSE(pack_2); CHECK(pack_2.error().Code() == LargeObjectErrorCode::FileNotFound); // There must be no spliced file: CHECK_FALSE(FileSystemManager::IsFile(path)); // Check implicit splice fails: auto spliced_path = kIsTree ? cas.TreePath(digest) : cas.BlobPath(digest, kIsExec); CHECK_FALSE(spliced_path); } } // Test splitting of an empty object. The split must be successful, but the // entry must not be placed to the LargeCAS. The result of splitting must be // empty. // The object cannot be implicitly reconstructed. template static void TestEmpty(Storage const& storage) noexcept { SECTION("Empty") { static constexpr bool kIsTree = IsTreeObject(kType); static constexpr bool kIsExec = IsExecutableObject(kType); using TestType = std::conditional_t>; // Create an empty file: auto temp_path = LargeTestUtils::Blob::Generate( TestType::kEmptyId, TestType::kEmptySize); REQUIRE(temp_path); auto const& cas = storage.CAS(); auto digest = kIsTree ? cas.StoreTree(*temp_path) : cas.StoreBlob(*temp_path, kIsExec); REQUIRE(digest); auto path = kIsTree ? cas.TreePath(*digest) : cas.BlobPath(*digest, kIsExec); REQUIRE(path); // Split the empty object: auto pack_1 = kIsTree ? cas.SplitTree(*digest) : cas.SplitBlob(*digest); CHECK(pack_1); CHECK(pack_1->empty()); // Test that there is no large entry in the storage: // To ensure there is no split of the initial object, it is removed: CHECK(FileSystemManager::RemoveFile(*path)); CHECK_FALSE(FileSystemManager::IsFile(*path)); // Split must not find the large entry: auto pack_2 = kIsTree ? cas.SplitTree(*digest) : cas.SplitBlob(*digest); CHECK_FALSE(pack_2); CHECK(pack_2.error().Code() == LargeObjectErrorCode::FileNotFound); // There must be no spliced file: CHECK_FALSE(FileSystemManager::IsFile(*path)); // Check implicit splice fails: auto spliced_path = kIsTree ? cas.TreePath(*digest) : cas.BlobPath(*digest, kIsExec); CHECK_FALSE(spliced_path); } } // Test splicing from an external source. // 1. The object can be explicitly spliced, if the parts are presented in the // storage. // 2. Explicit splice fails, it the result of splicing is different from // what was expected. // 3. Explicit splice fails, if some parts of the tree are missing. template static void TestExternal(StorageConfig const& storage_config, Storage const& storage) noexcept { SECTION("External") { static constexpr bool kIsTree = IsTreeObject(kType); static constexpr bool kIsExec = IsExecutableObject(kType); using TestType = std::conditional_t>; auto const& cas = storage.CAS(); // Create a large object: auto object = TestType::Create(cas, TestType::kLargeId, TestType::kLargeSize); CHECK(object); auto const& [digest, path] = *object; // Split the object: auto pack_1 = kIsTree ? cas.SplitTree(digest) : cas.SplitBlob(digest); CHECK(pack_1); CHECK(pack_1->size() > 1); // External source is emulated by moving the large entry to an older // generation and promoting the parts of the entry to the youngest // generation: REQUIRE(GarbageCollector::TriggerGarbageCollection(storage_config)); for (auto const& part : *pack_1) { static constexpr bool kIsExecutable = false; REQUIRE(cas.BlobPath(part, kIsExecutable)); } auto const youngest = ::Generation::Create(&storage_config); SECTION("Proper request") { if constexpr (kIsTree) { // Promote the parts of the tree: auto splice = cas.TreePath(digest); REQUIRE(splice); REQUIRE(FileSystemManager::RemoveFile(*splice)); } REQUIRE_FALSE(FileSystemManager::IsFile(path)); // Reconstruct the result from parts: std::ignore = kIsTree ? youngest.CAS().SpliceTree(digest, *pack_1) : youngest.CAS().SpliceBlob(digest, *pack_1, kIsExec); CHECK(FileSystemManager::IsFile(path)); } // Simulate a situation when parts result to an existing file, but it is // not the expected result: SECTION("Digest consistency fail") { // Splice the result to check it will not be affected: auto implicit_splice = kIsTree ? cas.TreePath(digest) : cas.BlobPath(digest, kIsExec); REQUIRE(implicit_splice); REQUIRE(*implicit_splice == path); // Randomize one more object to simulate invalidation: auto small = TestType::Create(cas, TestType::kSmallId, TestType::kSmallSize); REQUIRE(small); auto const& [small_digest, small_path] = *small; // The entry itself is not important, only it's digest is needed: REQUIRE(FileSystemManager::RemoveFile(small_path)); REQUIRE_FALSE(FileSystemManager::IsFile(small_path)); // Invalidation is simulated by reconstructing the small_digest // object from the parts of the initial object: auto splice = kIsTree ? youngest.CAS().SpliceTree(small_digest, *pack_1) : youngest.CAS().SpliceBlob(small_digest, *pack_1, kIsExec); REQUIRE_FALSE(splice); CHECK(splice.error().Code() == LargeObjectErrorCode::InvalidResult); // The initial entry must not be affected: REQUIRE(FileSystemManager::IsFile(path)); } if (kIsTree and ProtocolTraits::IsTreeAllowed( storage_config.hash_function.GetType())) { // Tree invariants check is omitted in compatible mode. SECTION("Tree invariants check fails") { // Check splice fails due to the tree invariants check. auto splice = youngest.CAS().SpliceTree(digest, *pack_1); REQUIRE_FALSE(splice); CHECK(splice.error().Code() == LargeObjectErrorCode::InvalidTree); } } } } // Test compactification of a storage generation. // If there are objects in the storage that have an entry in // the large CAS, they must be deleted during compactification. // All splitable objects in the generation must be split. template static void TestCompactification(StorageConfig const& storage_config, Storage const& storage) { SECTION("Compactify") { static constexpr bool kIsTree = IsTreeObject(kType); static constexpr bool kIsExec = IsExecutableObject(kType); using TestType = std::conditional_t>; auto const& cas = storage.CAS(); // Create a large object and split it: auto object = TestType::Create(cas, TestType::kLargeId, TestType::kLargeSize); REQUIRE(object); auto& [digest, path] = *object; auto result = kIsTree ? cas.SplitTree(digest) : cas.SplitBlob(digest); REQUIRE(result); // For trees the size must be increased to exceed the internal // compactification threshold: static constexpr auto kExceedThresholdSize = kIsTree ? TestType::kLargeSize * 8 : TestType::kLargeSize; // Create a large object that is to be split during compactification: auto object_2 = TestType::Create( cas, std::string(TestType::kLargeId) + "_2", kExceedThresholdSize); REQUIRE(object_2); auto& [digest_2, path_2] = *object_2; // After an interruption of a build process intermediate unique files // may be present in the storage. To ensure compactification deals with // them properly, a "unique" file is created: auto invalid_object = TestType::Create( cas, std::string(TestType::kLargeId) + "_3", kExceedThresholdSize); REQUIRE(invalid_object); auto& [invalid_digest, invalid_path] = *invalid_object; auto unique_path = CreateUniquePath(invalid_path); REQUIRE(unique_path); REQUIRE(FileSystemManager::Rename(invalid_path, *unique_path)); // Ensure all entries are in the storage: auto get_path = [](auto const& cas, ArtifactDigest const& digest) { return kIsTree ? cas.TreePath(digest) : cas.BlobPath(digest, kIsExec); }; auto const latest = ::Generation::Create(&storage_config); REQUIRE(get_path(latest.CAS(), digest).has_value()); REQUIRE(get_path(latest.CAS(), digest_2).has_value()); REQUIRE(FileSystemManager::IsFile(*unique_path)); // Compactify the youngest generation: // Generation rotation is disabled to exclude uplinking. static constexpr bool kNoRotation = true; REQUIRE(GarbageCollector::TriggerGarbageCollection(storage_config, kNoRotation)); // All entries must be deleted during compactification, and for blobs // and executables there are no synchronized entries in the storage: REQUIRE_FALSE(get_path(latest.CAS(), digest).has_value()); REQUIRE_FALSE(get_path(latest.CAS(), digest_2).has_value()); REQUIRE_FALSE(FileSystemManager::IsFile(*unique_path)); // All valid entries must be implicitly spliceable: REQUIRE(get_path(cas, digest).has_value()); REQUIRE(get_path(cas, digest_2).has_value()); } } TEST_CASE("LocalCAS: Split-Splice", "[storage]") { auto const config = TestStorageConfig::Create(); auto const storage = Storage::Create(&config.Get()); SECTION("File") { TestLarge(config.Get(), storage); TestSmall(storage); TestEmpty(storage); TestExternal(config.Get(), storage); TestCompactification(config.Get(), storage); } SECTION("Tree") { TestLarge(config.Get(), storage); TestSmall(storage); TestEmpty(storage); TestExternal(config.Get(), storage); TestCompactification(config.Get(), storage); } SECTION("Executable") { TestLarge(config.Get(), storage); TestSmall(storage); TestEmpty(storage); TestExternal(config.Get(), storage); TestCompactification(config.Get(), storage); } } TEST_CASE("Skip compactification", "[storage]") { auto const config = TestStorageConfig::Create(); auto const storage = Storage::Create(&config.Get()); auto const& cas = storage.CAS(); // Create a large object in CAS: using LargeTestUtils::File; auto object = File::Create(cas, File::kLargeId, File::kLargeSize); REQUIRE(object.has_value()); auto& [digest, path] = *object; REQUIRE(cas.BlobPath(digest, /*is_executable=*/false).has_value()); // Trigger garbage collection with --all REQUIRE(GarbageCollector::TriggerGarbageCollection( config.Get(), /*no_rotation=*/false, /*gc_all=*/true)); // Check no generation remains: for (std::size_t i = 0; i < config.Get().num_generations; ++i) { CHECK_FALSE( FileSystemManager::Exists(config.Get().GenerationCacheRoot(i))); } } // Test uplinking of nested large objects: // A large tree depends on a number of nested objects: // // large_tree // | - nested_blob // | - nested_tree // | |- other nested entries // | - other entries // // All large entries are preliminarily split and the spliced results are // deleted. The youngest generation is empty. Uplinking must restore the // object(and it's parts) and uplink them properly. TEST_CASE("LargeObjectCAS: uplink nested large objects", "[storage]") { auto const storage_config = TestStorageConfig::Create(); auto const storage = Storage::Create(&storage_config.Get()); auto const& cas = storage.CAS(); // Randomize a large directory: auto tree_path = LargeTestUtils::Tree::Generate( "nested_tree", LargeTestUtils::Tree::kLargeSize); REQUIRE(tree_path); // Randomize a large nested tree: auto const nested_tree = (*tree_path) / "nested_tree"; REQUIRE(LargeObjectUtils::GenerateDirectory( nested_tree, LargeTestUtils::Tree::kLargeSize)); // Randomize a large nested blob: auto nested_blob = (*tree_path) / "nested_blob"; REQUIRE(LargeObjectUtils::GenerateFile(nested_blob, LargeTestUtils::File::kLargeSize)); // Add the nested tree to the CAS: auto nested_tree_digest = LargeTestUtils::Tree::StoreRaw(cas, nested_tree); REQUIRE(nested_tree_digest); auto nested_tree_path = cas.TreePath(*nested_tree_digest); REQUIRE(nested_tree_path); // Add the nested blob to the CAS: auto nested_blob_digest = cas.StoreBlob(nested_blob, false); REQUIRE(nested_blob_digest); auto nested_blob_path = cas.BlobPath(*nested_blob_digest, false); REQUIRE(nested_blob_path); // Add the initial large directory to the CAS: auto large_tree_digest = LargeTestUtils::Tree::StoreRaw(cas, *tree_path); REQUIRE(large_tree_digest); auto large_tree_path = cas.TreePath(*large_tree_digest); REQUIRE(large_tree_path); // Split large entries: auto split_nested_tree = cas.SplitTree(*nested_tree_digest); REQUIRE(split_nested_tree); auto split_nested_blob = cas.SplitBlob(*nested_blob_digest); REQUIRE(split_nested_blob); auto split_large_tree = cas.SplitTree(*large_tree_digest); REQUIRE(split_large_tree); // Remove the spliced results: REQUIRE(FileSystemManager::RemoveFile(*nested_tree_path)); REQUIRE(FileSystemManager::RemoveFile(*nested_blob_path)); REQUIRE(FileSystemManager::RemoveFile(*large_tree_path)); // Rotate generations: REQUIRE(GarbageCollector::TriggerGarbageCollection(storage_config.Get())); // Ask to splice the large tree: auto result_path = cas.TreePath(*large_tree_digest); REQUIRE(result_path); // Only the main object must be reconstructed: CHECK(FileSystemManager::IsFile(*large_tree_path)); // It's parts must not be reconstructed by default: CHECK_FALSE(FileSystemManager::IsFile(*nested_tree_path)); CHECK_FALSE(FileSystemManager::IsFile(*nested_blob_path)); auto const latest = ::Generation::Create(&storage_config.Get()); // However, in native mode they might be reconstructed on request because // their entries are in the latest generation: if (ProtocolTraits::IsNative( storage_config.Get().hash_function.GetType())) { auto split_nested_tree_2 = latest.CAS().SplitTree(*nested_tree_digest); REQUIRE(split_nested_tree_2); auto split_nested_blob_2 = latest.CAS().SplitBlob(*nested_blob_digest); REQUIRE(split_nested_blob_2); } // Check there are no spliced results in old generations: for (std::size_t i = 1; i < storage_config.Get().num_generations; ++i) { auto const storage = ::Generation::Create(&storage_config.Get(), /*generation=*/i); auto const& generation_cas = storage.CAS(); REQUIRE_FALSE(generation_cas.TreePath(*nested_tree_digest)); REQUIRE_FALSE(generation_cas.TreePath(*large_tree_digest)); REQUIRE_FALSE(generation_cas.BlobPath(*nested_blob_digest, /*is_executable=*/false)); } } namespace { /// \brief Extends the lifetime of large files for the whole set of tests. class TestFilesDirectory final { public: [[nodiscard]] static auto Instance() noexcept -> TestFilesDirectory const& { static TestFilesDirectory directory; return directory; } [[nodiscard]] auto GetPath() const noexcept -> std::filesystem::path { return temp_directory_->GetPath(); } private: TmpDir::Ptr temp_directory_; explicit TestFilesDirectory() noexcept { auto test_dir = FileSystemManager::GetCurrentDirectory() / "tmp"; temp_directory_ = TmpDir::Create(test_dir / "tmp_space"); } }; namespace LargeTestUtils { template auto Blob::Create(LocalCAS const& cas, std::string const& id, std::uintmax_t size) noexcept -> std::optional> { std::optional path; while (not path.has_value()) { path = Generate(id, size); auto digest = path.has_value() ? ArtifactDigestFactory::HashFileAs( cas.GetHashFunction(), *path) : std::nullopt; if (not digest) { return std::nullopt; } if (cas.BlobPath(digest.value(), kIsExecutable).has_value()) { if (not FileSystemManager::RemoveFile(*path)) { return std::nullopt; } path.reset(); } } auto digest = path ? cas.StoreBlob(*path, kIsExecutable) : std::nullopt; auto blob_path = digest ? cas.BlobPath(*digest, kIsExecutable) : std::nullopt; if (digest and blob_path) { return std::make_pair(std::move(*digest), std::move(*blob_path)); } return std::nullopt; } template auto Blob::Generate(std::string const& id, std::uintmax_t size) noexcept -> std::optional { std::string const path_id = "blob" + id; auto path = TestFilesDirectory::Instance().GetPath() / path_id; if (FileSystemManager::IsFile(path) or LargeObjectUtils::GenerateFile(path, size)) { return path; } return std::nullopt; } auto Tree::Create(LocalCAS const& cas, std::string const& id, std::uintmax_t entries_count) noexcept -> std::optional> { auto path = Generate(id, entries_count); auto digest = path ? StoreRaw(cas, *path) : std::nullopt; auto cas_path = digest ? cas.TreePath(*digest) : std::nullopt; if (digest and cas_path) { return std::make_pair(std::move(*digest), std::move(*cas_path)); } return std::nullopt; } auto Tree::Generate(std::string const& id, std::uintmax_t entries_count) noexcept -> std::optional { std::string const path_id = "tree" + id; auto path = TestFilesDirectory::Instance().GetPath() / path_id; if (FileSystemManager::IsDirectory(path) or LargeObjectUtils::GenerateDirectory(path, entries_count)) { return path; } return std::nullopt; } auto Tree::StoreRaw(LocalCAS const& cas, std::filesystem::path const& directory) noexcept -> std::optional { if (not FileSystemManager::IsDirectory(directory)) { return std::nullopt; } auto store_blob = [&cas](std::filesystem::path const& path, auto is_exec) -> std::optional { return cas.StoreBlob(path, is_exec); }; auto store_tree = [&cas](std::string const& content) -> std::optional { return cas.StoreTree(content); }; auto store_symlink = [&cas](std::string const& content) -> std::optional { return cas.StoreBlob(content); }; return ProtocolTraits::IsNative(cas.GetHashFunction().GetType()) ? BazelMsgFactory::CreateGitTreeDigestFromLocalTree( directory, store_blob, store_tree, store_symlink) : BazelMsgFactory::CreateDirectoryDigestFromLocalTree( directory, store_blob, store_tree, store_symlink); } } // namespace LargeTestUtils } // namespace