From f3708dfc53c919509bf4dd18448abb31e048af03 Mon Sep 17 00:00:00 2001 From: Maksim Denisov Date: Wed, 11 Dec 2024 15:31:14 +0100 Subject: Test computation of TreeStructure --- .../tree_structure/tree_structure.test.cpp | 242 ++++++++++++++++++++- 1 file changed, 241 insertions(+), 1 deletion(-) (limited to 'test/buildtool/tree_structure/tree_structure.test.cpp') diff --git a/test/buildtool/tree_structure/tree_structure.test.cpp b/test/buildtool/tree_structure/tree_structure.test.cpp index e0656c69..8943d4a9 100644 --- a/test/buildtool/tree_structure/tree_structure.test.cpp +++ b/test/buildtool/tree_structure/tree_structure.test.cpp @@ -12,22 +12,37 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include +#include #include #include +#include #include #include #include +#include +#include #include "catch2/catch_test_macros.hpp" #include "gsl/gsl" #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/file_system/file_system_manager.hpp" +#include "src/buildtool/file_system/git_repo.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/storage.hpp" +#include "src/buildtool/tree_structure/compute_tree_structure.hpp" #include "src/buildtool/tree_structure/tree_structure_cache.hpp" +#include "src/utils/cpp/expected.hpp" +#include "src/utils/cpp/hash_combine.hpp" +#include "src/utils/cpp/hex_string.hpp" +#include "src/utils/cpp/path.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" @@ -37,7 +52,33 @@ namespace { Storage const& storage, std::uintmax_t entries) -> std::optional; -} + +[[nodiscard]] auto CreateComplexTestDirectory( + StorageConfig const& storage_config, + Storage const& storage) -> std::optional; + +[[nodiscard]] auto ValidateTreeStructure(ArtifactDigest const& digest, + Storage const& storage) -> bool; + +struct TreeEntryHasher { + [[nodiscard]] auto operator()( + GitRepo::TreeEntry const& entry) const noexcept -> std::size_t { + size_t seed{}; + hash_combine(&seed, entry.name); + hash_combine(&seed, entry.type); + return seed; + } +}; + +using TreeEntriesHitContainer = + std::unordered_map; + +[[nodiscard]] auto CountTreeEntries( + ArtifactDigest const& digest, + Storage const& storage, + gsl::not_null const& container, + bool increment) -> bool; +} // namespace TEST_CASE("cache", "[tree_structure]") { auto const storage_config = TestStorageConfig::Create(); @@ -80,6 +121,41 @@ TEST_CASE("cache", "[tree_structure]") { CHECK(youngest.CAS().TreePath(*to_dir).has_value()); } +TEST_CASE("compute", "[tree_structure]") { + auto const storage_config = TestStorageConfig::Create(); + if (not ProtocolTraits::IsNative( + storage_config.Get().hash_function.GetType())) { + return; + } + + auto const storage = Storage::Create(&storage_config.Get()); + TreeStructureCache const ts_cache(&storage_config.Get()); + + auto const tree = CreateComplexTestDirectory(storage_config.Get(), storage); + REQUIRE(tree); + + auto const tree_structure = ComputeTreeStructure(*tree, storage, ts_cache); + REQUIRE(tree_structure); + + REQUIRE(ValidateTreeStructure(*tree_structure, storage)); + + TreeEntriesHitContainer container; + // Add recursively all TreeEntries of the source tree to the container, + // incrementing counters: + REQUIRE(CountTreeEntries(*tree, storage, &container, /*increment=*/true)); + // Add recursively all TreeEntries of the tree structure to the container, + // decrementing counters: + REQUIRE(CountTreeEntries( + *tree_structure, storage, &container, /*increment=*/false)); + + // All counters must be equal to 0, meaning all entries have been hit. + auto const all_hit_equally = + std::all_of(container.begin(), container.end(), [](auto const& p) { + return p.second == 0; + }); + REQUIRE(all_hit_equally); +} + namespace { [[nodiscard]] auto CreateDirectory(std::filesystem::path const& directory, Storage const& storage) { @@ -116,4 +192,168 @@ namespace { } return CreateDirectory(tree->GetPath(), storage); } + +[[nodiscard]] auto CreateComplexTestDirectory( + StorageConfig const& storage_config, + Storage const& storage) -> std::optional { + auto const test_dir = storage_config.CreateTypedTmpDir("tmp"); + auto head_temp_directory = TmpDir::Create(test_dir->GetPath() / "head_dir"); + auto const head_temp_dir_path = head_temp_directory->GetPath(); + // ├── exec_1 + // ├── file_1 + // ├── symlink_to_nested_dir_1_1 -> nested_dir_1 / nested_dir_1_1 + // ├── symlink_to_nested_dir_2_1 -> nested_dir_2 / nested_dir_2_1 + // ├── nested_dir_1 + // │ ├── ... + // │ ├── nested_dir_1_1 + // │ │ └── ... + // │ └── nested_dir_1_2 + // │ └── ... + // └── nested_dir_2 + // ├── ... + // ├── nested_dir_2_1 + // │ └── ... + // └── nested_dir_2_2 + // └── ... + static constexpr std::size_t kFileSize = 128; + auto const file_path = head_temp_dir_path / "file_1"; + if (not LargeObjectUtils::GenerateFile(file_path, kFileSize)) { + return std::nullopt; + } + + auto const exec_path = head_temp_dir_path / "exec_1"; + if (not LargeObjectUtils::GenerateFile(exec_path, + kFileSize, + /*is_executable =*/true)) { + return std::nullopt; + } + + std::array const directories = { + head_temp_dir_path / "nested_dir_1", + head_temp_dir_path / "nested_dir_1" / "nested_dir_1_1", + head_temp_dir_path / "nested_dir_1" / "nested_dir_1_2", + head_temp_dir_path / "nested_dir_2", + head_temp_dir_path / "nested_dir_2" / "nested_dir_2_1", + head_temp_dir_path / "nested_dir_2" / "nested_dir_2_2"}; + + static constexpr std::size_t kDirEntries = 16; + for (auto const& path : directories) { + if (not LargeObjectUtils::GenerateDirectory(path, kDirEntries)) { + return std::nullopt; + } + } + + // Create non-upwards symlinks in the top directory: + if (not FileSystemManager::CreateNonUpwardsSymlink( + std::filesystem::path("nested_dir_1") / "nested_dir_1_1", + head_temp_dir_path / "symlink_to_nested_dir_1_1") or + not FileSystemManager::CreateNonUpwardsSymlink( + std::filesystem::path("nested_dir_2") / "nested_dir_2_1", + head_temp_dir_path / "symlink_to_nested_dir_2_1")) { + return std::nullopt; + } + return CreateDirectory(head_temp_dir_path, storage); +} + +[[nodiscard]] auto ReadGitTree(Storage const& storage, + ArtifactDigest const& tree) + -> std::optional { + auto const tree_path = storage.CAS().TreePath(tree); + if (not tree_path) { + return std::nullopt; + } + + auto const tree_content = FileSystemManager::ReadFile(*tree_path); + if (not tree_content) { + return std::nullopt; + } + + auto const check_symlinks = + [&storage](std::vector const& ids) { + return std::all_of( + ids.begin(), ids.end(), [&storage](auto const& id) -> bool { + auto path_to_symlink = + storage.CAS().BlobPath(id, /*is_executable=*/false); + if (not path_to_symlink) { + return false; + } + auto const content = + FileSystemManager::ReadFile(*path_to_symlink); + return content and PathIsNonUpwards(*content); + }); + }; + return GitRepo::ReadTreeData(*tree_content, + tree.hash(), + check_symlinks, + /*is_hex_id=*/true); +} + +[[nodiscard]] auto ValidateTreeStructure(ArtifactDigest const& digest, + Storage const& storage) -> bool { + auto tree_entries = ReadGitTree(storage, digest); + if (not tree_entries) { + return false; + } + + auto const empty_blob_hash = ArtifactDigest{}.hash(); + for (auto const& [raw_id, es] : *tree_entries) { + auto const hex_id = ToHexString(raw_id); + for (auto const& entry : es) { + switch (entry.type) { + case ObjectType::Tree: { + auto const git_digest = ArtifactDigestFactory::Create( + HashFunction::Type::GitSHA1, + hex_id, + /*size is unknown*/ 0, + IsTreeObject(entry.type)); + if (not git_digest or + not ValidateTreeStructure(*git_digest, storage)) { + return false; + } + } break; + default: { + if (hex_id != empty_blob_hash) { + return false; + } + } + } + } + } + return true; +} + +[[nodiscard]] auto CountTreeEntries( + ArtifactDigest const& digest, + Storage const& storage, + gsl::not_null const& container, + bool increment) -> bool { + auto tree_entries = ReadGitTree(storage, digest); + if (not tree_entries) { + return false; + } + + auto const empty_blob_hash = + ArtifactDigestFactory::HashDataAs( + storage.GetHashFunction(), std::string{}); + for (auto const& [raw_id, es] : *tree_entries) { + auto const hex_id = ToHexString(raw_id); + for (auto const& entry : es) { + if (IsTreeObject(entry.type)) { + auto const git_digest = + ArtifactDigestFactory::Create(HashFunction::Type::GitSHA1, + hex_id, + /*size is unknown*/ 0, + IsTreeObject(entry.type)); + if (not git_digest or + not CountTreeEntries( + *git_digest, storage, container, increment)) { + return false; + } + } + increment ? ++(*container)[entry] : --(*container)[entry]; + } + } + return true; +} + } // namespace -- cgit v1.2.3