summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOliver Reiche <oliver.reiche@huawei.com>2025-05-09 19:08:30 +0200
committerOliver Reiche <oliver.reiche@huawei.com>2025-06-24 14:56:58 +0200
commit9a4c2d306c1981df3684bd0a3bb42a53dd9ce7ed (patch)
tree6bb02c896d65adc59953b2f68f8bcbb5fcb07d27
parenteded9c836a82a9c0d4d0bc41b160b03f3cd72422 (diff)
downloadjustbuild-9a4c2d306c1981df3684bd0a3bb42a53dd9ce7ed.tar.gz
Test: Add test for execution server APIs
-rw-r--r--test/buildtool/execution_api/execution_service/TARGETS42
-rw-r--r--test/buildtool/execution_api/execution_service/execution_server.test.cpp340
2 files changed, 381 insertions, 1 deletions
diff --git a/test/buildtool/execution_api/execution_service/TARGETS b/test/buildtool/execution_api/execution_service/TARGETS
index c71380c4..e9ea3a5d 100644
--- a/test/buildtool/execution_api/execution_service/TARGETS
+++ b/test/buildtool/execution_api/execution_service/TARGETS
@@ -27,9 +27,49 @@
]
, "stage": ["test", "buildtool", "execution_api", "execution_service"]
}
+, "execution_server":
+ { "type": ["@", "rules", "CC/test", "test"]
+ , "name": ["execution_server"]
+ , "srcs": ["execution_server.test.cpp"]
+ , "private-deps":
+ [ ["@", "catch2", "", "catch2"]
+ , ["@", "fmt", "", "fmt"]
+ , ["@", "grpc", "", "grpc++"]
+ , ["@", "gsl", "", "gsl"]
+ , ["@", "protoc", "", "libprotobuf"]
+ , ["@", "src", "src/buildtool/common", "bazel_digest_factory"]
+ , ["@", "src", "src/buildtool/common", "bazel_types"]
+ , ["@", "src", "src/buildtool/common", "common"]
+ , ["@", "src", "src/buildtool/common", "protocol_traits"]
+ , ["@", "src", "src/buildtool/crypto", "hash_function"]
+ , [ "@"
+ , "src"
+ , "src/buildtool/execution_api/execution_service"
+ , "cas_server"
+ ]
+ , [ "@"
+ , "src"
+ , "src/buildtool/execution_api/execution_service"
+ , "execution_server"
+ ]
+ , ["@", "src", "src/buildtool/execution_api/local", "config"]
+ , ["@", "src", "src/buildtool/execution_api/local", "context"]
+ , ["@", "src", "src/buildtool/execution_api/local", "local_api"]
+ , ["@", "src", "src/buildtool/execution_api/remote", "bazel_network"]
+ , ["@", "src", "src/buildtool/file_system", "git_repo"]
+ , ["@", "src", "src/buildtool/file_system", "object_type"]
+ , ["@", "src", "src/buildtool/storage", "config"]
+ , ["@", "src", "src/buildtool/storage", "storage"]
+ , ["@", "src", "src/utils/cpp", "expected"]
+ , ["", "catch-main"]
+ , ["utils", "test_hash_function_type"]
+ , ["utils", "test_storage_config"]
+ ]
+ , "stage": ["test", "buildtool", "execution_api", "execution_service"]
+ }
, "TESTS":
{ "type": ["@", "rules", "test", "suite"]
, "stage": ["execution_service"]
- , "deps": ["cas_server"]
+ , "deps": ["cas_server", "execution_server"]
}
}
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 <algorithm>
+#include <compare>
+#include <cstdlib>
+#include <functional>
+#include <iterator>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include <grpcpp/grpcpp.h>
+
+// 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 <ObjectType kType>
+[[nodiscard]] auto Upload(
+ gsl::not_null<bazel_re::ContentAddressableStorage::Service*> const&
+ cas_server,
+ std::string const& instance_name,
+ gsl::not_null<const StorageConfig*> const& storage_config,
+ std::string const& content) noexcept -> std::optional<bazel_re::Digest> {
+ auto digest = BazelDigestFactory::HashDataAs<kType>(
+ 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<bazel_re::ContentAddressableStorage::Service*> const&
+ cas_server,
+ gsl::not_null<const StorageConfig*> 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<ObjectType::Tree>(
+ cas_server, instance_name, storage_config, empty_tree->second);
+ REQUIRE(digest);
+ return *digest;
+ }
+ auto digest =
+ Upload<ObjectType::File>(cas_server,
+ instance_name,
+ storage_config,
+ bazel_re::Directory{}.SerializeAsString());
+ REQUIRE(digest);
+ return *digest;
+}
+
+[[nodiscard]] auto Execute(
+ gsl::not_null<bazel_re::ContentAddressableStorage::Service*> const&
+ cas_server,
+ gsl::not_null<bazel_re::Execution::Service*> const& exec_server,
+ gsl::not_null<const StorageConfig*> const& storage_config,
+ std::string const& instance_name,
+ bazel_re::Digest const& root_digest,
+ std::string const& cwd,
+ std::vector<std::string> const& argv,
+ std::vector<std::string> output_files,
+ std::vector<std::string> output_dirs,
+ std::map<std::string, std::string> const& env,
+ std::map<std::string, std::string> const& properties,
+ Capabilities::Version const& version) noexcept
+ -> std::optional<ArtifactDigest> {
+ auto get_platform = [&properties]() {
+ auto platform = std::make_unique<bazel_re::Platform>();
+ 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<std::string>{};
+ 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<ObjectType::File>(
+ 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<ObjectType::File>(
+ 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<std::string, std::string>{};
+ 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<std::string>{};
+ 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");
+ }
+ }
+ }
+}