From 9a4c2d306c1981df3684bd0a3bb42a53dd9ce7ed Mon Sep 17 00:00:00 2001 From: Oliver Reiche Date: Fri, 9 May 2025 19:08:30 +0200 Subject: Test: Add test for execution server APIs --- .../execution_service/execution_server.test.cpp | 340 +++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 test/buildtool/execution_api/execution_service/execution_server.test.cpp (limited to 'test/buildtool/execution_api/execution_service/execution_server.test.cpp') diff --git a/test/buildtool/execution_api/execution_service/execution_server.test.cpp b/test/buildtool/execution_api/execution_service/execution_server.test.cpp new file mode 100644 index 00000000..506802d9 --- /dev/null +++ b/test/buildtool/execution_api/execution_service/execution_server.test.cpp @@ -0,0 +1,340 @@ +// Copyright 2025 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/execution_api/execution_service/execution_server.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// Don't include "proto" +// IWYU pragma: no_include "google/longrunning/operations.pb.h" +// IWYU pragma: no_include "build/bazel/remote/execution/v2/remote_execution.grpc.pb.h" + +#include "catch2/catch_test_macros.hpp" +#include "catch2/catch_tostring.hpp" +#include "catch2/generators/catch_generators_all.hpp" +#include "fmt/core.h" +#include "google/protobuf/repeated_ptr_field.h" +#include "gsl/gsl" +#include "src/buildtool/common/artifact_digest.hpp" +#include "src/buildtool/common/artifact_digest_factory.hpp" +#include "src/buildtool/common/bazel_digest_factory.hpp" +#include "src/buildtool/common/bazel_types.hpp" +#include "src/buildtool/common/protocol_traits.hpp" +#include "src/buildtool/crypto/hash_function.hpp" +#include "src/buildtool/execution_api/execution_service/cas_server.hpp" +#include "src/buildtool/execution_api/local/config.hpp" +#include "src/buildtool/execution_api/local/context.hpp" +#include "src/buildtool/execution_api/local/local_api.hpp" +#include "src/buildtool/execution_api/remote/bazel/bazel_capabilities_client.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/storage.hpp" +#include "src/utils/cpp/expected.hpp" +#include "test/utils/hermeticity/test_hash_function_type.hpp" +#include "test/utils/hermeticity/test_storage_config.hpp" + +namespace { + +auto const kV20 = Capabilities::Version{.major = 2, .minor = 0, .patch = 0}; +auto const kV21 = Capabilities::Version{.major = 2, .minor = 1, .patch = 0}; + +// Class to obtain a valid pointer to internal ServerWriter<...::Operation> +class MockServerWriter final + : public ::grpc::ServerWriterInterface<::google::longrunning::Operation> { + public: + MockServerWriter() = default; + [[nodiscard]] auto Get() + -> ::grpc::ServerWriter<::google::longrunning::Operation>* { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast< + ::grpc::ServerWriter<::google::longrunning::Operation>*>(this); + } + + // stub implementations + void SendInitialMetadata() override {} + using ::grpc::internal::WriterInterface< + google::longrunning::Operation>::Write; + auto Write(google::longrunning::Operation const& /*msg*/, + grpc::WriteOptions /*options*/) -> bool override { + return true; + } + + private: + MockServerWriter(grpc::internal::Call* /*call*/, + grpc::ServerContext* /*ctx*/) {} +}; + +template +[[nodiscard]] auto Upload( + gsl::not_null const& + cas_server, + std::string const& instance_name, + gsl::not_null const& storage_config, + std::string const& content) noexcept -> std::optional { + auto digest = BazelDigestFactory::HashDataAs( + storage_config->hash_function, content); + auto request = bazel_re::BatchUpdateBlobsRequest{}; + request.set_instance_name(instance_name); + auto* req = request.add_requests(); + req->mutable_digest()->CopyFrom(digest); + req->set_data(content); + auto response = bazel_re::BatchUpdateBlobsResponse{}; + if (cas_server->BatchUpdateBlobs(nullptr, &request, &response).ok()) { + return digest; + } + return std::nullopt; +} + +[[nodiscard]] auto CreateEmptyTree( + gsl::not_null const& + cas_server, + gsl::not_null const& storage_config, + std::string const& instance_name) noexcept { + if (ProtocolTraits::IsNative(TestHashType::ReadFromEnvironment())) { + auto empty_entries = GitRepo::tree_entries_t{}; + auto empty_tree = GitRepo::CreateShallowTree(empty_entries); + REQUIRE(empty_tree); + auto digest = Upload( + cas_server, instance_name, storage_config, empty_tree->second); + REQUIRE(digest); + return *digest; + } + auto digest = + Upload(cas_server, + instance_name, + storage_config, + bazel_re::Directory{}.SerializeAsString()); + REQUIRE(digest); + return *digest; +} + +[[nodiscard]] auto Execute( + gsl::not_null const& + cas_server, + gsl::not_null const& exec_server, + gsl::not_null const& storage_config, + std::string const& instance_name, + bazel_re::Digest const& root_digest, + std::string const& cwd, + std::vector const& argv, + std::vector output_files, + std::vector output_dirs, + std::map const& env, + std::map const& properties, + Capabilities::Version const& version) noexcept + -> std::optional { + auto get_platform = [&properties]() { + auto platform = std::make_unique(); + std::transform(properties.begin(), + properties.end(), + pb::back_inserter(platform->mutable_properties()), + [](auto prop) { + auto out = bazel_re::Platform_Property{}; + out.set_name(prop.first); + out.set_value(prop.second); + return out; + }); + return platform.release(); + }; + + // create command + auto cmd = bazel_re::Command{}; + std::copy( + argv.begin(), argv.end(), pb::back_inserter(cmd.mutable_arguments())); + cmd.set_working_directory(cwd); + std::transform(env.begin(), + env.end(), + pb::back_inserter(cmd.mutable_environment_variables()), + [](auto const& name_val) { + auto var = bazel_re::Command_EnvironmentVariable{}; + var.set_name(name_val.first); + var.set_value(name_val.second); + return var; + }); + if (version >= kV21) { + auto paths = std::vector{}; + paths.reserve(output_files.size() + output_dirs.size()); + paths.insert(paths.end(), + std::move_iterator{output_files.begin()}, + std::move_iterator{output_files.end()}); + paths.insert(paths.end(), + std::move_iterator{output_dirs.begin()}, + std::move_iterator{output_dirs.end()}); + std::sort(paths.begin(), paths.end()); + std::copy(paths.begin(), + paths.end(), + pb::back_inserter(cmd.mutable_output_paths())); + } + else { + std::sort(output_files.begin(), output_files.end()); + std::sort(output_dirs.begin(), output_dirs.end()); + std::copy(output_files.begin(), + output_files.end(), + pb::back_inserter(cmd.mutable_output_files())); + std::copy(output_dirs.begin(), + output_dirs.end(), + pb::back_inserter(cmd.mutable_output_directories())); + } + cmd.set_allocated_platform(get_platform()); + auto cmd_digest = Upload( + cas_server, instance_name, storage_config, cmd.SerializeAsString()); + REQUIRE(cmd_digest); + + // create action + auto action = bazel_re::Action{}; + action.mutable_command_digest()->CopyFrom(*cmd_digest); + action.mutable_input_root_digest()->CopyFrom(root_digest); + auto action_digest = Upload( + cas_server, instance_name, storage_config, action.SerializeAsString()); + REQUIRE(action_digest); + + // create execute request + auto request = bazel_re::ExecuteRequest{}; + request.set_instance_name(instance_name); + request.mutable_action_digest()->CopyFrom(*action_digest); + + // mock server-internal execute call + auto writer = MockServerWriter{}; + auto status = exec_server->Execute(nullptr, &request, writer.Get()); + if (status.ok()) { + if (auto just_digest = ArtifactDigestFactory::FromBazel( + storage_config->hash_function.GetType(), *action_digest)) { + return *std::move(just_digest); + } + } + return std::nullopt; +} + +[[nodiscard]] auto ToString(Capabilities::Version const& version) + -> std::string { + return fmt::format("{}.{}.{}", version.major, version.minor, version.patch); +} + +} // namespace + +TEST_CASE("Execution Service: Test supported API versions", + "[execution_service]") { + auto const storage_config = TestStorageConfig::Create(); + auto const storage = Storage::Create(&storage_config.Get()); + LocalExecutionConfig const local_exec_config{}; + + // pack the local context instances to be passed + LocalContext const local_context{.exec_config = &local_exec_config, + .storage_config = &storage_config.Get(), + .storage = &storage}; + + auto local_api = LocalApi{&local_context}; + auto exec_server = + ExecutionServiceImpl{&local_context, &local_api, std::nullopt}; + + auto cas_server = CASServiceImpl{&local_context}; + auto instance_name = std::string{"remote-execution"}; + + auto root_digest = + CreateEmptyTree(&cas_server, &storage_config.Get(), instance_name); + + auto env = std::map{}; + if (auto const* path_var = std::getenv("PATH")) { + // server executes locally, make sure it knows about PATH from TEST_ENV + env.emplace("PATH", path_var); + } + + auto version = GENERATE(kV20, kV21); + + DYNAMIC_SECTION("Pretend being a client using RBEv" << ToString(version)) { + auto action_digest = + Execute(&cas_server, + &exec_server, + &storage_config.Get(), + instance_name, + root_digest, + "", + {"/bin/sh", + "-c", + "set -e; touch foo; ln -s none fox; " + "mkdir -p bar; rm -rf bat; ln -s none bat"}, + {"foo", "fox"}, + {"bar", "bat"}, + env, + {}, + version); + REQUIRE(action_digest); + + auto result = storage.ActionCache().CachedResult(*action_digest); + REQUIRE(result); + + // check output files and directories + CHECK_FALSE(result->output_files().empty()); + CHECK_FALSE(result->output_directories().empty()); + if (not result->output_files().empty()) { + CHECK(result->output_files().begin()->path() == "foo"); + } + if (not result->output_directories().empty()) { + CHECK(result->output_directories().begin()->path() == "bar"); + } + + // check output symlinks + if (version >= kV21) { + // starting from RBEv2.1, output_symlinks must be filled + CHECK(result->output_symlinks_size() == 2); + if (result->output_symlinks_size() == 2) { + auto paths = std::unordered_set{}; + paths.reserve(2); + std::transform(result->output_symlinks().begin(), + result->output_symlinks().end(), + std::inserter(paths, paths.end()), + [](auto const& link) { return link.path(); }); + CHECK(paths.contains("fox")); + CHECK(paths.contains("bat")); + } + + // separated file/dir symlinks may be reported additionally + if (not result->output_file_symlinks().empty()) { + CHECK(result->output_file_symlinks().begin()->path() == "fox"); + } + if (not result->output_directory_symlinks().empty()) { + CHECK(result->output_directory_symlinks().begin()->path() == + "bat"); + } + } + else { + // in legacy mode, output_symlinks must not be set... + CHECK(result->output_symlinks().empty()); + // ... instead, file/dir symlinks must be reported separately + CHECK_FALSE(result->output_file_symlinks().empty()); + CHECK_FALSE(result->output_directory_symlinks().empty()); + if (not result->output_file_symlinks().empty()) { + CHECK(result->output_file_symlinks().begin()->path() == "fox"); + } + if (not result->output_directory_symlinks().empty()) { + CHECK(result->output_directory_symlinks().begin()->path() == + "bat"); + } + } + } +} -- cgit v1.2.3