summaryrefslogtreecommitdiff
path: root/test/buildtool/execution_engine/executor
diff options
context:
space:
mode:
Diffstat (limited to 'test/buildtool/execution_engine/executor')
-rw-r--r--test/buildtool/execution_engine/executor/TARGETS71
-rw-r--r--test/buildtool/execution_engine/executor/data/greeter/greet.cpp6
-rw-r--r--test/buildtool/execution_engine/executor/data/greeter/greet.hpp3
-rw-r--r--test/buildtool/execution_engine/executor/data/greeter/greet_mod.cpp8
-rw-r--r--test/buildtool/execution_engine/executor/data/greeter/main.cpp6
-rw-r--r--test/buildtool/execution_engine/executor/data/hello_world/main.cpp6
-rwxr-xr-xtest/buildtool/execution_engine/executor/executor.test.cpp358
-rwxr-xr-xtest/buildtool/execution_engine/executor/executor_api.test.hpp615
-rwxr-xr-xtest/buildtool/execution_engine/executor/executor_api_local.test.cpp36
-rwxr-xr-xtest/buildtool/execution_engine/executor/executor_api_remote_bazel.test.cpp71
10 files changed, 1180 insertions, 0 deletions
diff --git a/test/buildtool/execution_engine/executor/TARGETS b/test/buildtool/execution_engine/executor/TARGETS
new file mode 100644
index 00000000..9bf1c50e
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/TARGETS
@@ -0,0 +1,71 @@
+{ "executor_api_tests":
+ { "type": ["@", "rules", "CC", "library"]
+ , "name": ["executor_api_tests"]
+ , "hdrs": ["executor_api.test.hpp"]
+ , "stage": ["test", "buildtool", "execution_engine", "executor"]
+ }
+, "executor":
+ { "type": ["@", "rules", "CC/test", "test"]
+ , "name": ["executor"]
+ , "srcs": ["executor.test.cpp"]
+ , "deps":
+ [ ["src/buildtool/common", "artifact_factory"]
+ , ["src/buildtool/execution_api/common", "common"]
+ , ["src/buildtool/execution_engine/dag", "dag"]
+ , ["src/buildtool/execution_engine/executor", "executor"]
+ , ["test", "catch-main"]
+ , ["@", "catch2", "", "catch2"]
+ ]
+ , "stage": ["test", "buildtool", "execution_engine", "executor"]
+ }
+, "local":
+ { "type": ["@", "rules", "CC/test", "test"]
+ , "name": ["local"]
+ , "srcs": ["executor_api_local.test.cpp"]
+ , "data": ["test_data"]
+ , "deps":
+ [ "executor_api_tests"
+ , ["src/buildtool/common", "artifact_factory"]
+ , ["src/buildtool/execution_api/local", "local"]
+ , ["src/buildtool/execution_api/remote", "config"]
+ , ["src/buildtool/execution_engine/dag", "dag"]
+ , ["src/buildtool/execution_engine/executor", "executor"]
+ , ["test/utils", "catch-main-remote-execution"]
+ , ["test/utils", "local_hermeticity"]
+ , ["@", "catch2", "", "catch2"]
+ ]
+ , "stage": ["test", "buildtool", "execution_engine", "executor"]
+ }
+, "remote_bazel":
+ { "type": ["@", "rules", "CC/test", "test"]
+ , "name": ["remote_bazel"]
+ , "srcs": ["executor_api_remote_bazel.test.cpp"]
+ , "data": ["test_data"]
+ , "deps":
+ [ "executor_api_tests"
+ , ["src/buildtool/common", "artifact_factory"]
+ , ["src/buildtool/execution_api/remote", "bazel"]
+ , ["src/buildtool/execution_api/remote", "config"]
+ , ["src/buildtool/execution_engine/executor", "executor"]
+ , ["test/utils", "catch-main-remote-execution"]
+ , ["@", "catch2", "", "catch2"]
+ ]
+ , "stage": ["test", "buildtool", "execution_engine", "executor"]
+ }
+, "test_data":
+ { "type": ["@", "rules", "data", "staged"]
+ , "srcs":
+ [ "data/greeter/greet.cpp"
+ , "data/greeter/greet.hpp"
+ , "data/greeter/greet_mod.cpp"
+ , "data/greeter/main.cpp"
+ , "data/hello_world/main.cpp"
+ ]
+ , "stage": ["test", "buildtool", "execution_engine", "executor"]
+ }
+, "TESTS":
+ { "type": "install"
+ , "tainted": ["test"]
+ , "deps": ["executor", "local", "remote_bazel"]
+ }
+} \ No newline at end of file
diff --git a/test/buildtool/execution_engine/executor/data/greeter/greet.cpp b/test/buildtool/execution_engine/executor/data/greeter/greet.cpp
new file mode 100644
index 00000000..f1a1cf6b
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/data/greeter/greet.cpp
@@ -0,0 +1,6 @@
+#include <iostream>
+#include "greet.hpp"
+
+void greet(std::string const& name) {
+ std::cout << "Hello " << name << std::endl;
+}
diff --git a/test/buildtool/execution_engine/executor/data/greeter/greet.hpp b/test/buildtool/execution_engine/executor/data/greeter/greet.hpp
new file mode 100644
index 00000000..d4cb767d
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/data/greeter/greet.hpp
@@ -0,0 +1,3 @@
+#include <string>
+
+void greet(std::string const& name);
diff --git a/test/buildtool/execution_engine/executor/data/greeter/greet_mod.cpp b/test/buildtool/execution_engine/executor/data/greeter/greet_mod.cpp
new file mode 100644
index 00000000..550a7bf8
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/data/greeter/greet_mod.cpp
@@ -0,0 +1,8 @@
+#include <iostream>
+#include "greet.hpp"
+
+// this is a modification that has no effect on the produced binary
+
+void greet(std::string const& name) {
+ std::cout << "Hello " << name << std::endl;
+}
diff --git a/test/buildtool/execution_engine/executor/data/greeter/main.cpp b/test/buildtool/execution_engine/executor/data/greeter/main.cpp
new file mode 100644
index 00000000..4d51ee4a
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/data/greeter/main.cpp
@@ -0,0 +1,6 @@
+#include "greet.hpp"
+
+int main(void) {
+ greet("devcloud");
+ return 0;
+}
diff --git a/test/buildtool/execution_engine/executor/data/hello_world/main.cpp b/test/buildtool/execution_engine/executor/data/hello_world/main.cpp
new file mode 100644
index 00000000..f7eb16a1
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/data/hello_world/main.cpp
@@ -0,0 +1,6 @@
+#include <iostream>
+
+int main(void) {
+ std::cout << "Hello World!" << std::endl;
+ return 0;
+}
diff --git a/test/buildtool/execution_engine/executor/executor.test.cpp b/test/buildtool/execution_engine/executor/executor.test.cpp
new file mode 100755
index 00000000..63f41521
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/executor.test.cpp
@@ -0,0 +1,358 @@
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "catch2/catch.hpp"
+#include "src/buildtool/common/artifact_factory.hpp"
+#include "src/buildtool/execution_api/common/execution_api.hpp"
+#include "src/buildtool/execution_engine/executor/executor.hpp"
+#include "src/buildtool/file_system/file_system_manager.hpp"
+
+/// \brief Mockup API test config.
+struct TestApiConfig {
+ struct TestArtifactConfig {
+ bool uploads{};
+ bool available{};
+ };
+
+ struct TestExecutionConfig {
+ bool failed{};
+ std::vector<std::string> outputs{};
+ };
+
+ struct TestResponseConfig {
+ bool cached{};
+ int exit_code{};
+ };
+
+ std::unordered_map<std::string, TestArtifactConfig> artifacts{};
+ TestExecutionConfig execution;
+ TestResponseConfig response;
+};
+
+// forward declarations
+class TestApi;
+class TestAction;
+class TestResponse;
+
+/// \brief Mockup Response, stores only config and action result
+class TestResponse : public IExecutionResponse {
+ friend class TestAction;
+
+ public:
+ [[nodiscard]] auto Status() const noexcept -> StatusCode final {
+ return StatusCode::Success;
+ }
+ [[nodiscard]] auto ExitCode() const noexcept -> int final {
+ return config_.response.exit_code;
+ }
+ [[nodiscard]] auto IsCached() const noexcept -> bool final {
+ return config_.response.cached;
+ }
+ [[nodiscard]] auto HasStdErr() const noexcept -> bool final { return true; }
+ [[nodiscard]] auto HasStdOut() const noexcept -> bool final { return true; }
+ [[nodiscard]] auto StdErr() noexcept -> std::string final { return {}; }
+ [[nodiscard]] auto StdOut() noexcept -> std::string final { return {}; }
+ [[nodiscard]] auto ActionDigest() const noexcept -> std::string final {
+ return {};
+ }
+ [[nodiscard]] auto Artifacts() const noexcept -> ArtifactInfos final {
+ ArtifactInfos artifacts{};
+ artifacts.reserve(config_.execution.outputs.size());
+
+ // collect files and store them
+ for (auto const& path : config_.execution.outputs) {
+ try {
+ artifacts.emplace(path,
+ Artifact::ObjectInfo{ArtifactDigest{path, 0},
+ ObjectType::File});
+ } catch (...) {
+ return {};
+ }
+ }
+
+ return artifacts;
+ }
+
+ private:
+ TestApiConfig config_{};
+ explicit TestResponse(TestApiConfig config) noexcept
+ : config_{std::move(config)} {}
+};
+
+/// \brief Mockup Action, stores only config
+class TestAction : public IExecutionAction {
+ friend class TestApi;
+
+ public:
+ auto Execute(Logger const* /*unused*/) noexcept
+ -> IExecutionResponse::Ptr final {
+ if (config_.execution.failed) {
+ return nullptr;
+ }
+ return IExecutionResponse::Ptr{new TestResponse{config_}};
+ }
+ void SetCacheFlag(CacheFlag /*unused*/) noexcept final {}
+ void SetTimeout(std::chrono::milliseconds /*unused*/) noexcept final {}
+
+ private:
+ TestApiConfig config_{};
+ explicit TestAction(TestApiConfig config) noexcept
+ : config_{std::move(config)} {}
+};
+
+/// \brief Mockup Api, use config to create action and handle artifact upload
+class TestApi : public IExecutionApi {
+ public:
+ explicit TestApi(TestApiConfig config) noexcept
+ : config_{std::move(config)} {}
+
+ auto CreateAction(
+ ArtifactDigest const& /*unused*/,
+ std::vector<std::string> const& /*unused*/,
+ std::vector<std::string> const& /*unused*/,
+ std::vector<std::string> const& /*unused*/,
+ std::map<std::string, std::string> const& /*unused*/,
+ std::map<std::string, std::string> const& /*unused*/) noexcept
+ -> IExecutionAction::Ptr final {
+ return IExecutionAction::Ptr{new TestAction(config_)};
+ }
+ auto RetrieveToPaths(
+ std::vector<Artifact::ObjectInfo> const& /*unused*/,
+ std::vector<std::filesystem::path> const& /*unused*/) noexcept
+ -> bool final {
+ return false; // not needed by Executor
+ }
+ auto RetrieveToFds(std::vector<Artifact::ObjectInfo> const& /*unused*/,
+ std::vector<int> const& /*unused*/) noexcept
+ -> bool final {
+ return false; // not needed by Executor
+ }
+ auto Upload(BlobContainer const& blobs, bool /*unused*/) noexcept
+ -> bool final {
+ for (auto const& blob : blobs) {
+ if (config_.artifacts[blob.data].uploads) {
+ continue; // for local artifacts
+ }
+ if (config_.artifacts[blob.digest.hash()].uploads) {
+ continue; // for known and action artifacts
+ }
+ return false;
+ }
+ return true;
+ }
+ auto UploadTree(
+ std::vector<
+ DependencyGraph::NamedArtifactNodePtr> const& /*unused*/) noexcept
+ -> std::optional<ArtifactDigest> final {
+ return ArtifactDigest{}; // not needed by Executor
+ }
+ [[nodiscard]] auto IsAvailable(ArtifactDigest const& digest) const noexcept
+ -> bool final {
+ try {
+ return config_.artifacts.at(digest.hash()).available;
+ } catch (std::exception const& /* unused */) {
+ return false;
+ }
+ }
+
+ private:
+ TestApiConfig config_{};
+};
+
+static void SetupConfig(std::filesystem::path const& ws) {
+ auto info = RepositoryConfig::RepositoryInfo{FileRoot{ws}};
+ RepositoryConfig::Instance().Reset();
+ RepositoryConfig::Instance().SetInfo("", std::move(info));
+}
+
+[[nodiscard]] static auto CreateTest(gsl::not_null<DependencyGraph*> const& g,
+ std::filesystem::path const& ws)
+ -> TestApiConfig {
+ using path = std::filesystem::path;
+ SetupConfig(ws);
+
+ auto const local_cpp_desc = ArtifactDescription{path{"local.cpp"}, ""};
+ auto const local_cpp_id = local_cpp_desc.Id();
+
+ auto const known_cpp_desc =
+ ArtifactDescription{ArtifactDigest{"known.cpp", 0}, ObjectType::File};
+ auto const known_cpp_id = known_cpp_desc.Id();
+
+ auto const test_action_desc = ActionDescription{
+ {"output1.exe", "output2.exe"},
+ {},
+ Action{"test_action", {"cmd", "line"}, {}},
+ {{"local.cpp", local_cpp_desc}, {"known.cpp", known_cpp_desc}}};
+
+ CHECK(g->AddAction(test_action_desc));
+ CHECK(FileSystemManager::WriteFile("local.cpp", ws / "local.cpp"));
+
+ TestApiConfig config{};
+
+ config.artifacts["local.cpp"].uploads = true;
+ config.artifacts["known.cpp"].available = true;
+ config.artifacts["output1.exe"].available = true;
+ config.artifacts["output2.exe"].available = true;
+
+ config.execution.failed = false;
+ config.execution.outputs = {"output1.exe", "output2.exe"};
+
+ config.response.cached = true;
+ config.response.exit_code = 0;
+
+ return config;
+}
+
+TEST_CASE("Executor: Process artifact", "[executor]") {
+ std::filesystem::path workspace_path{
+ "test/buildtool/execution_engine/executor"};
+ DependencyGraph g;
+ auto config = CreateTest(&g, workspace_path);
+
+ auto const local_cpp_desc =
+ ArtifactFactory::DescribeLocalArtifact("local.cpp", "");
+ auto const local_cpp_id = ArtifactFactory::Identifier(local_cpp_desc);
+
+ auto const known_cpp_desc = ArtifactFactory::DescribeKnownArtifact(
+ "known.cpp", 0, ObjectType::File);
+ auto const known_cpp_id = ArtifactFactory::Identifier(known_cpp_desc);
+
+ SECTION("Processing succeeds for valid config") {
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ }
+
+ SECTION("Processing fails if uploading local artifact failed") {
+ config.artifacts["local.cpp"].uploads = false;
+
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(not runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ }
+
+ SECTION("Processing fails if known artifact is not available") {
+ config.artifacts["known.cpp"].available = false;
+
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(not runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ }
+}
+
+TEST_CASE("Executor: Process action", "[executor]") {
+ std::filesystem::path workspace_path{
+ "test/buildtool/execution_engine/executor"};
+
+ DependencyGraph g;
+ auto config = CreateTest(&g, workspace_path);
+
+ auto const local_cpp_desc =
+ ArtifactFactory::DescribeLocalArtifact("local.cpp", "");
+ auto const local_cpp_id = ArtifactFactory::Identifier(local_cpp_desc);
+
+ auto const known_cpp_desc = ArtifactFactory::DescribeKnownArtifact(
+ "known.cpp", 0, ObjectType::File);
+ auto const known_cpp_id = ArtifactFactory::Identifier(known_cpp_desc);
+
+ ActionIdentifier action_id{"test_action"};
+ auto const output1_desc =
+ ArtifactFactory::DescribeActionArtifact(action_id, "output1.exe");
+ auto const output1_id = ArtifactFactory::Identifier(output1_desc);
+
+ auto const output2_desc =
+ ArtifactFactory::DescribeActionArtifact(action_id, "output2.exe");
+ auto const output2_id = ArtifactFactory::Identifier(output2_desc);
+
+ SECTION("Processing succeeds for valid config") {
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ CHECK(runner.Process(g.ActionNodeWithId(action_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(output1_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(output2_id)));
+ }
+
+ SECTION("Processing succeeds even if result was is not cached") {
+ config.response.cached = false;
+
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ CHECK(runner.Process(g.ActionNodeWithId(action_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(output1_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(output2_id)));
+ }
+
+ SECTION("Processing succeeds even if output is not available in CAS") {
+ config.artifacts["output2.exe"].available = false;
+
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ CHECK(runner.Process(g.ActionNodeWithId(action_id)));
+
+ // Note: Both output digests should be created via SaveDigests(),
+ // but processing output2.exe fails as it is not available in CAS.
+ CHECK(runner.Process(g.ArtifactNodeWithId(output1_id)));
+ CHECK(not runner.Process(g.ArtifactNodeWithId(output2_id)));
+ }
+
+ SECTION("Processing fails if execution failed") {
+ config.execution.failed = true;
+
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ CHECK(not runner.Process(g.ActionNodeWithId(action_id)));
+ CHECK(not runner.Process(g.ArtifactNodeWithId(output1_id)));
+ CHECK(not runner.Process(g.ArtifactNodeWithId(output2_id)));
+ }
+
+ SECTION("Processing fails if exit code is non-zero") {
+ config.response.exit_code = 1;
+
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ CHECK(not runner.Process(g.ActionNodeWithId(action_id)));
+
+ // Note: Both output digests should be missing as SaveDigests() for
+ // both is only called if processing action succeeds.
+ CHECK(not runner.Process(g.ArtifactNodeWithId(output1_id)));
+ CHECK(not runner.Process(g.ArtifactNodeWithId(output2_id)));
+ }
+
+ SECTION("Processing fails if any output is missing") {
+ config.execution.outputs = {"output1.exe" /*, "output2.exe"*/};
+
+ auto api = TestApi::Ptr{new TestApi{config}};
+ Executor runner{api.get(), {}};
+
+ CHECK(runner.Process(g.ArtifactNodeWithId(local_cpp_id)));
+ CHECK(runner.Process(g.ArtifactNodeWithId(known_cpp_id)));
+ CHECK(not runner.Process(g.ActionNodeWithId(action_id)));
+
+ // Note: Both output digests should be missing as SaveDigests() for
+ // both is only called if processing action succeeds.
+ CHECK(not runner.Process(g.ArtifactNodeWithId(output1_id)));
+ CHECK(not runner.Process(g.ArtifactNodeWithId(output2_id)));
+ }
+}
diff --git a/test/buildtool/execution_engine/executor/executor_api.test.hpp b/test/buildtool/execution_engine/executor/executor_api.test.hpp
new file mode 100755
index 00000000..48d37d98
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/executor_api.test.hpp
@@ -0,0 +1,615 @@
+#ifndef INCLUDED_SRC_TEST_BUILDTOOL_EXECUTION_ENGINE_EXECUTOR_EXECUTOR_API_TEST_HPP
+#define INCLUDED_SRC_TEST_BUILDTOOL_EXECUTION_ENGINE_EXECUTOR_EXECUTOR_API_TEST_HPP
+
+#include <functional>
+
+#include "catch2/catch.hpp"
+#include "src/buildtool/common/artifact.hpp"
+#include "src/buildtool/common/artifact_description.hpp"
+#include "src/buildtool/common/artifact_factory.hpp"
+#include "src/buildtool/execution_api/common/execution_api.hpp"
+#include "src/buildtool/execution_engine/dag/dag.hpp"
+#include "src/buildtool/execution_engine/executor/executor.hpp"
+#include "src/buildtool/file_system/file_system_manager.hpp"
+#include "test/utils/test_env.hpp"
+
+using ApiFactory = std::function<IExecutionApi::Ptr()>;
+
+static inline void SetupConfig() {
+ auto info = RepositoryConfig::RepositoryInfo{
+ FileRoot{"test/buildtool/execution_engine/executor"}};
+ RepositoryConfig::Instance().SetInfo("", std::move(info));
+}
+
+static inline void RunBlobUpload(ApiFactory const& factory) {
+ SetupConfig();
+ auto api = factory();
+ std::string const blob = "test";
+ CHECK(api->Upload(BlobContainer{
+ {BazelBlob{ArtifactDigest{ComputeHash(blob), blob.size()}, blob}}}));
+}
+
+[[nodiscard]] static inline auto GetTestDir() -> std::filesystem::path {
+ auto* tmp_dir = std::getenv("TEST_TMPDIR");
+ if (tmp_dir != nullptr) {
+ return tmp_dir;
+ }
+ return FileSystemManager::GetCurrentDirectory() /
+ "test/buildtool/execution_engine/executor";
+}
+
+template <class Executor>
+[[nodiscard]] static inline auto AddAndProcessTree(DependencyGraph* g,
+ Executor* runner,
+ Tree const& tree_desc)
+ -> std::optional<Artifact::ObjectInfo> {
+ REQUIRE(g->AddAction(tree_desc.Action()));
+
+ // obtain tree action and tree artifact
+ auto const* tree_action = g->ActionNodeWithId(tree_desc.Id());
+ REQUIRE_FALSE(tree_action == nullptr);
+ auto const* tree_artifact = g->ArtifactNodeWithId(tree_desc.Output().Id());
+ REQUIRE_FALSE(tree_artifact == nullptr);
+
+ // "run" tree action to produce tree artifact
+ REQUIRE(runner->Process(tree_action));
+
+ // read computed tree artifact info (digest + object type)
+ return tree_artifact->Content().Info();
+}
+
+static inline void RunHelloWorldCompilation(ApiFactory const& factory,
+ bool is_hermetic = true,
+ int expected_queued = 0,
+ int expected_cached = 0) {
+ using path = std::filesystem::path;
+ SetupConfig();
+ auto const main_cpp_desc =
+ ArtifactDescription{path{"data/hello_world/main.cpp"}, ""};
+ auto const main_cpp_id = main_cpp_desc.Id();
+ std::string const make_hello_id = "make_hello";
+ auto const make_hello_desc = ActionDescription{
+ {"out/hello_world"},
+ {},
+ Action{make_hello_id,
+ {"c++", "src/main.cpp", "-o", "out/hello_world"},
+ {{"PATH", "/bin:/usr/bin"}}},
+ {{"src/main.cpp", main_cpp_desc}}};
+ auto const exec_desc =
+ ArtifactDescription{make_hello_id, "out/hello_world"};
+ auto const exec_id = exec_desc.Id();
+
+ DependencyGraph g;
+ CHECK(g.AddAction(make_hello_desc));
+ CHECK(g.ArtifactNodeWithId(exec_id)->HasBuilderAction());
+
+ auto api = factory();
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+
+ // upload local artifacts
+ auto const* main_cpp_node = g.ArtifactNodeWithId(main_cpp_id);
+ CHECK(main_cpp_node != nullptr);
+ CHECK(runner.Process(main_cpp_node));
+
+ // process action
+ CHECK(runner.Process(g.ArtifactNodeWithId(exec_id)->BuilderActionNode()));
+ if (is_hermetic) {
+ CHECK(Statistics::Instance().ActionsQueuedCounter() == expected_queued);
+ CHECK(Statistics::Instance().ActionsCachedCounter() == expected_cached);
+ }
+
+ auto tmpdir = GetTestDir();
+
+ // retrieve ALL artifacts
+ REQUIRE(FileSystemManager::CreateDirectory(tmpdir));
+ for (auto const& artifact_id : g.ArtifactIdentifiers()) {
+ CHECK(api->RetrieveToPaths(
+ {*g.ArtifactNodeWithId(artifact_id)->Content().Info()},
+ {(tmpdir / "output").string()}));
+ CHECK(FileSystemManager::IsFile(tmpdir / "output"));
+ REQUIRE(FileSystemManager::RemoveFile(tmpdir / "output"));
+ }
+}
+
+static inline void RunGreeterCompilation(ApiFactory const& factory,
+ std::string const& greetcpp,
+ bool is_hermetic = true,
+ int expected_queued = 0,
+ int expected_cached = 0) {
+ using path = std::filesystem::path;
+ SetupConfig();
+ auto const greet_hpp_desc =
+ ArtifactDescription{path{"data/greeter/greet.hpp"}, ""};
+ auto const greet_hpp_id = greet_hpp_desc.Id();
+ auto const greet_cpp_desc =
+ ArtifactDescription{path{"data/greeter"} / greetcpp, ""};
+ auto const greet_cpp_id = greet_cpp_desc.Id();
+
+ std::string const compile_greet_id = "compile_greet";
+ auto const compile_greet_desc =
+ ActionDescription{{"out/greet.o"},
+ {},
+ Action{compile_greet_id,
+ {"c++",
+ "-c",
+ "src/greet.cpp",
+ "-I",
+ "include",
+ "-o",
+ "out/greet.o"},
+ {{"PATH", "/bin:/usr/bin"}}},
+ {{"include/greet.hpp", greet_hpp_desc},
+ {"src/greet.cpp", greet_cpp_desc}}};
+
+ auto const greet_o_desc =
+ ArtifactDescription{compile_greet_id, "out/greet.o"};
+ auto const greet_o_id = greet_o_desc.Id();
+
+ std::string const make_lib_id = "make_lib";
+ auto const make_lib_desc = ActionDescription{
+ {"out/libgreet.a"},
+ {},
+ Action{make_lib_id, {"ar", "rcs", "out/libgreet.a", "greet.o"}, {}},
+ {{"greet.o", greet_o_desc}}};
+
+ auto const main_cpp_desc =
+ ArtifactDescription{path{"data/greeter/main.cpp"}, ""};
+ auto const main_cpp_id = main_cpp_desc.Id();
+
+ auto const libgreet_desc =
+ ArtifactDescription{make_lib_id, "out/libgreet.a"};
+ auto const libgreet_id = libgreet_desc.Id();
+
+ std::string const make_exe_id = "make_exe";
+ auto const make_exe_desc =
+ ActionDescription{{"out/greeter"},
+ {},
+ Action{make_exe_id,
+ {"c++",
+ "src/main.cpp",
+ "-I",
+ "include",
+ "-L",
+ "lib",
+ "-lgreet",
+ "-o",
+ "out/greeter"},
+ {{"PATH", "/bin:/usr/bin"}}},
+ {{"src/main.cpp", main_cpp_desc},
+ {"include/greet.hpp", greet_hpp_desc},
+ {"lib/libgreet.a", libgreet_desc}}};
+
+ auto const exec_id = ArtifactDescription(make_exe_id, "out/greeter").Id();
+
+ DependencyGraph g;
+ CHECK(g.Add({compile_greet_desc, make_lib_desc, make_exe_desc}));
+
+ auto api = factory();
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+
+ // upload local artifacts
+ for (auto const& id : {greet_hpp_id, greet_cpp_id, main_cpp_id}) {
+ auto const* node = g.ArtifactNodeWithId(id);
+ CHECK(node != nullptr);
+ CHECK(runner.Process(node));
+ }
+
+ // process actions
+ CHECK(
+ runner.Process(g.ArtifactNodeWithId(greet_o_id)->BuilderActionNode()));
+ CHECK(
+ runner.Process(g.ArtifactNodeWithId(libgreet_id)->BuilderActionNode()));
+ CHECK(runner.Process(g.ArtifactNodeWithId(exec_id)->BuilderActionNode()));
+ if (is_hermetic) {
+ CHECK(Statistics::Instance().ActionsQueuedCounter() == expected_queued);
+ CHECK(Statistics::Instance().ActionsCachedCounter() == expected_cached);
+ }
+
+ auto tmpdir = GetTestDir();
+
+ // retrieve ALL artifacts
+ REQUIRE(FileSystemManager::CreateDirectory(tmpdir));
+ for (auto const& artifact_id : g.ArtifactIdentifiers()) {
+ CHECK(api->RetrieveToPaths(
+ {*g.ArtifactNodeWithId(artifact_id)->Content().Info()},
+ {(tmpdir / "output").string()}));
+ CHECK(FileSystemManager::IsFile(tmpdir / "output"));
+ REQUIRE(FileSystemManager::RemoveFile(tmpdir / "output"));
+ }
+}
+
+[[maybe_unused]] static void TestBlobUpload(ApiFactory const& factory) {
+ SetupConfig();
+ // NOLINTNEXTLINE
+ RunBlobUpload(factory);
+}
+
+[[maybe_unused]] static void TestHelloWorldCompilation(
+ ApiFactory const& factory,
+ bool is_hermetic = true) {
+ SetupConfig();
+ // expecting 1 action queued, 0 results from cache
+ // NOLINTNEXTLINE
+ RunHelloWorldCompilation(factory, is_hermetic, 1, 0);
+
+ SECTION("Running same compilation again") {
+ // expecting 2 actions queued, 1 result from cache
+ // NOLINTNEXTLINE
+ RunHelloWorldCompilation(factory, is_hermetic, 2, 1);
+ }
+}
+
+[[maybe_unused]] static void TestGreeterCompilation(ApiFactory const& factory,
+ bool is_hermetic = true) {
+ SetupConfig();
+ // expecting 3 action queued, 0 results from cache
+ // NOLINTNEXTLINE
+ RunGreeterCompilation(factory, "greet.cpp", is_hermetic, 3, 0);
+
+ SECTION("Running same compilation again") {
+ // expecting 6 actions queued, 3 results from cache
+ // NOLINTNEXTLINE
+ RunGreeterCompilation(factory, "greet.cpp", is_hermetic, 6, 3);
+ }
+
+ SECTION("Running modified compilation") {
+ // expecting 6 actions queued, 2 results from cache
+ // NOLINTNEXTLINE
+ RunGreeterCompilation(factory, "greet_mod.cpp", is_hermetic, 6, 2);
+ }
+}
+
+static inline void TestUploadAndDownloadTrees(ApiFactory const& factory,
+ bool /*is_hermetic*/ = true,
+ int /*expected_queued*/ = 0,
+ int /*expected_cached*/ = 0) {
+ SetupConfig();
+ auto tmpdir = GetTestDir();
+
+ auto foo = std::string{"foo"};
+ auto bar = std::string{"bar"};
+ auto foo_digest = ArtifactDigest{ComputeHash(foo), foo.size()};
+ auto bar_digest = ArtifactDigest{ComputeHash(bar), bar.size()};
+
+ // upload blobs
+ auto api = factory();
+ REQUIRE(api->Upload(BlobContainer{
+ {BazelBlob{foo_digest, foo}, BazelBlob{bar_digest, bar}}}));
+
+ // define known artifacts
+ auto foo_desc = ArtifactDescription{foo_digest, ObjectType::File};
+ auto bar_desc = ArtifactDescription{bar_digest, ObjectType::File};
+
+ DependencyGraph g{};
+ auto foo_id = g.AddArtifact(foo_desc);
+ auto bar_id = g.AddArtifact(bar_desc);
+
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+ REQUIRE(runner.Process(g.ArtifactNodeWithId(foo_id)));
+ REQUIRE(runner.Process(g.ArtifactNodeWithId(bar_id)));
+
+ SECTION("Simple tree") {
+ auto tree_desc = Tree{{{"a", foo_desc}, {"b", bar_desc}}};
+ auto tree_info = AddAndProcessTree(&g, &runner, tree_desc);
+ REQUIRE(tree_info);
+ CHECK(IsTreeObject(tree_info->type));
+
+ tmpdir /= "simple";
+ CHECK(api->RetrieveToPaths({*tree_info}, {tmpdir.string()}));
+ CHECK(FileSystemManager::IsDirectory(tmpdir));
+ CHECK(FileSystemManager::IsFile(tmpdir / "a"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "b"));
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "a") == "foo");
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "b") == "bar");
+ REQUIRE(FileSystemManager::RemoveDirectory(tmpdir, true));
+ }
+
+ SECTION("Subdir in tree path") {
+ auto tree_desc = Tree{{{"a", foo_desc}, {"b/a", bar_desc}}};
+ auto tree_info = AddAndProcessTree(&g, &runner, tree_desc);
+ REQUIRE(tree_info);
+ CHECK(IsTreeObject(tree_info->type));
+
+ tmpdir /= "subdir";
+ CHECK(api->RetrieveToPaths({*tree_info}, {tmpdir.string()}));
+ CHECK(FileSystemManager::IsDirectory(tmpdir));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "b"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "a"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "b" / "a"));
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "a") == "foo");
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "b" / "a") == "bar");
+ REQUIRE(FileSystemManager::RemoveDirectory(tmpdir, true));
+ }
+
+ SECTION("Nested trees") {
+ auto tree_desc_nested = Tree{{{"a", bar_desc}}};
+ auto tree_desc_parent =
+ Tree{{{"a", foo_desc}, {"b", tree_desc_nested.Output()}}};
+
+ REQUIRE(AddAndProcessTree(&g, &runner, tree_desc_nested));
+ auto tree_info = AddAndProcessTree(&g, &runner, tree_desc_parent);
+ REQUIRE(tree_info);
+ CHECK(IsTreeObject(tree_info->type));
+
+ tmpdir /= "nested";
+ CHECK(api->RetrieveToPaths({*tree_info}, {tmpdir.string()}));
+ CHECK(FileSystemManager::IsDirectory(tmpdir));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "b"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "a"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "b" / "a"));
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "a") == "foo");
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "b" / "a") == "bar");
+ REQUIRE(FileSystemManager::RemoveDirectory(tmpdir, true));
+ }
+
+ SECTION("Dot-path tree as action input") {
+ auto tree_desc = Tree{{{"a", foo_desc}, {"b/a", bar_desc}}};
+ auto action_inputs =
+ ActionDescription::inputs_t{{".", tree_desc.Output()}};
+ ActionDescription action_desc{
+ {"a", "b/a"}, {}, Action{"action_id", {"echo"}, {}}, action_inputs};
+
+ REQUIRE(AddAndProcessTree(&g, &runner, tree_desc));
+ REQUIRE(g.Add({action_desc}));
+ auto const* action_node = g.ActionNodeWithId("action_id");
+ REQUIRE(runner.Process(action_node));
+
+ tmpdir /= "dotpath";
+ std::vector<Artifact::ObjectInfo> infos{};
+ std::vector<std::filesystem::path> paths{};
+ for (auto const& [path, node] : action_node->OutputFiles()) {
+ paths.emplace_back(tmpdir / path);
+ infos.emplace_back(*node->Content().Info());
+ }
+
+ CHECK(api->RetrieveToPaths(infos, paths));
+ CHECK(FileSystemManager::IsDirectory(tmpdir));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "b"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "a"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "b" / "a"));
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "a") == "foo");
+ CHECK(*FileSystemManager::ReadFile(tmpdir / "b" / "a") == "bar");
+ REQUIRE(FileSystemManager::RemoveDirectory(tmpdir, true));
+ }
+
+ SECTION("Dot-path non-tree as action input") {
+ auto action_inputs = ActionDescription::inputs_t{{".", foo_desc}};
+ ActionDescription action_desc{
+ {"foo"}, {}, Action{"action_id", {"echo"}, {}}, action_inputs};
+
+ REQUIRE(g.Add({action_desc}));
+ auto const* action_node = g.ActionNodeWithId("action_id");
+ REQUIRE_FALSE(runner.Process(action_node));
+ }
+}
+
+static inline void TestRetrieveOutputDirectories(ApiFactory const& factory,
+ bool /*is_hermetic*/ = true,
+ int /*expected_queued*/ = 0,
+ int /*expected_cached*/ = 0) {
+ SetupConfig();
+ auto tmpdir = GetTestDir();
+
+ auto const make_tree_id = std::string{"make_tree"};
+ auto const* make_tree_cmd =
+ "mkdir -p baz/baz/\n"
+ "touch foo bar\n"
+ "touch baz/foo baz/bar\n"
+ "touch baz/baz/foo baz/baz/bar";
+
+ auto create_action = [&make_tree_id, make_tree_cmd](
+ std::vector<std::string>&& out_files,
+ std::vector<std::string>&& out_dirs) {
+ return ActionDescription{std::move(out_files),
+ std::move(out_dirs),
+ Action{make_tree_id,
+ {"sh", "-c", make_tree_cmd},
+ {{"PATH", "/bin:/usr/bin"}}},
+ {}};
+ };
+
+ SECTION("entire action output as directory") {
+ auto const make_tree_desc = create_action({}, {""});
+ auto const root_desc = ArtifactDescription{make_tree_id, ""};
+
+ DependencyGraph g{};
+ REQUIRE(g.AddAction(make_tree_desc));
+
+ auto const* action = g.ActionNodeWithId(make_tree_id);
+ REQUIRE_FALSE(action == nullptr);
+ auto const* root = g.ArtifactNodeWithId(root_desc.Id());
+ REQUIRE_FALSE(root == nullptr);
+
+ // run action
+ auto api = factory();
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+ REQUIRE(runner.Process(action));
+
+ // read output
+ auto root_info = root->Content().Info();
+ REQUIRE(root_info);
+ CHECK(IsTreeObject(root_info->type));
+
+ // retrieve ALL artifacts
+ auto tmpdir = GetTestDir() / "entire_output";
+ REQUIRE(FileSystemManager::CreateDirectory(tmpdir));
+
+ REQUIRE(api->RetrieveToPaths({*root_info}, {tmpdir}));
+ CHECK(FileSystemManager::IsFile(tmpdir / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "bar"));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "baz"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "bar"));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "baz" / "baz"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "baz" / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "baz" / "bar"));
+ }
+
+ SECTION("disjoint files and directories") {
+ auto const make_tree_desc = create_action({"foo", "bar"}, {"baz"});
+ auto const foo_desc = ArtifactDescription{make_tree_id, "foo"};
+ auto const bar_desc = ArtifactDescription{make_tree_id, "bar"};
+ auto const baz_desc = ArtifactDescription{make_tree_id, "baz"};
+
+ DependencyGraph g{};
+ REQUIRE(g.AddAction(make_tree_desc));
+
+ auto const* action = g.ActionNodeWithId(make_tree_id);
+ REQUIRE_FALSE(action == nullptr);
+ auto const* foo = g.ArtifactNodeWithId(foo_desc.Id());
+ REQUIRE_FALSE(foo == nullptr);
+ auto const* bar = g.ArtifactNodeWithId(bar_desc.Id());
+ REQUIRE_FALSE(bar == nullptr);
+ auto const* baz = g.ArtifactNodeWithId(baz_desc.Id());
+ REQUIRE_FALSE(baz == nullptr);
+
+ // run action
+ auto api = factory();
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+ REQUIRE(runner.Process(action));
+
+ // read output
+ auto foo_info = foo->Content().Info();
+ REQUIRE(foo_info);
+ CHECK(IsFileObject(foo_info->type));
+
+ auto bar_info = bar->Content().Info();
+ REQUIRE(bar_info);
+ CHECK(IsFileObject(bar_info->type));
+
+ auto baz_info = baz->Content().Info();
+ REQUIRE(baz_info);
+ CHECK(IsTreeObject(baz_info->type));
+
+ // retrieve ALL artifacts
+ auto tmpdir = GetTestDir() / "disjoint";
+ REQUIRE(FileSystemManager::CreateDirectory(tmpdir));
+
+ REQUIRE(api->RetrieveToPaths({*foo_info}, {tmpdir / "foo"}));
+ CHECK(FileSystemManager::IsFile(tmpdir / "foo"));
+
+ REQUIRE(api->RetrieveToPaths({*bar_info}, {tmpdir / "bar"}));
+ CHECK(FileSystemManager::IsFile(tmpdir / "bar"));
+
+ REQUIRE(api->RetrieveToPaths({*baz_info}, {tmpdir / "baz"}));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "baz"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "bar"));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "baz" / "baz"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "baz" / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "baz" / "bar"));
+ }
+
+ SECTION("nested files and directories") {
+ auto const make_tree_desc =
+ create_action({"foo", "baz/bar"}, {"", "baz/baz"});
+ auto const root_desc = ArtifactDescription{make_tree_id, ""};
+ auto const foo_desc = ArtifactDescription{make_tree_id, "foo"};
+ auto const bar_desc = ArtifactDescription{make_tree_id, "baz/bar"};
+ auto const baz_desc = ArtifactDescription{make_tree_id, "baz/baz"};
+
+ DependencyGraph g{};
+ REQUIRE(g.AddAction(make_tree_desc));
+
+ auto const* action = g.ActionNodeWithId(make_tree_id);
+ REQUIRE_FALSE(action == nullptr);
+ auto const* root = g.ArtifactNodeWithId(root_desc.Id());
+ REQUIRE_FALSE(root == nullptr);
+ auto const* foo = g.ArtifactNodeWithId(foo_desc.Id());
+ REQUIRE_FALSE(foo == nullptr);
+ auto const* bar = g.ArtifactNodeWithId(bar_desc.Id());
+ REQUIRE_FALSE(bar == nullptr);
+ auto const* baz = g.ArtifactNodeWithId(baz_desc.Id());
+ REQUIRE_FALSE(baz == nullptr);
+
+ // run action
+ auto api = factory();
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+ REQUIRE(runner.Process(action));
+
+ // read output
+ auto root_info = root->Content().Info();
+ REQUIRE(root_info);
+ CHECK(IsTreeObject(root_info->type));
+
+ auto foo_info = foo->Content().Info();
+ REQUIRE(foo_info);
+ CHECK(IsFileObject(foo_info->type));
+
+ auto bar_info = bar->Content().Info();
+ REQUIRE(bar_info);
+ CHECK(IsFileObject(bar_info->type));
+
+ auto baz_info = baz->Content().Info();
+ REQUIRE(baz_info);
+ CHECK(IsTreeObject(baz_info->type));
+
+ // retrieve ALL artifacts
+ auto tmpdir = GetTestDir() / "baz";
+ REQUIRE(FileSystemManager::CreateDirectory(tmpdir));
+
+ REQUIRE(api->RetrieveToPaths({*root_info}, {tmpdir / "root"}));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "root"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "root" / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "root" / "bar"));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "root" / "baz"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "root" / "baz" / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "root" / "baz" / "bar"));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "root" / "baz" / "baz"));
+ CHECK(
+ FileSystemManager::IsFile(tmpdir / "root" / "baz" / "baz" / "foo"));
+ CHECK(
+ FileSystemManager::IsFile(tmpdir / "root" / "baz" / "baz" / "bar"));
+
+ REQUIRE(api->RetrieveToPaths({*foo_info}, {tmpdir / "foo"}));
+ CHECK(FileSystemManager::IsFile(tmpdir / "foo"));
+
+ REQUIRE(api->RetrieveToPaths({*bar_info}, {tmpdir / "bar"}));
+ CHECK(FileSystemManager::IsFile(tmpdir / "bar"));
+
+ REQUIRE(api->RetrieveToPaths({*baz_info}, {tmpdir / "baz"}));
+ CHECK(FileSystemManager::IsDirectory(tmpdir / "baz"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "foo"));
+ CHECK(FileSystemManager::IsFile(tmpdir / "baz" / "bar"));
+ }
+
+ SECTION("non-existing outputs") {
+ SECTION("non-existing file") {
+ auto const make_tree_desc = create_action({"fool"}, {});
+ auto const fool_desc = ArtifactDescription{make_tree_id, "fool"};
+
+ DependencyGraph g{};
+ REQUIRE(g.AddAction(make_tree_desc));
+
+ auto const* action = g.ActionNodeWithId(make_tree_id);
+ REQUIRE_FALSE(action == nullptr);
+ auto const* fool = g.ArtifactNodeWithId(fool_desc.Id());
+ REQUIRE_FALSE(fool == nullptr);
+
+ // run action
+ auto api = factory();
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+ CHECK_FALSE(runner.Process(action));
+ }
+
+ SECTION("non-existing directory") {
+ auto const make_tree_desc = create_action({"bazel"}, {});
+ auto const bazel_desc = ArtifactDescription{make_tree_id, "bazel"};
+
+ DependencyGraph g{};
+ REQUIRE(g.AddAction(make_tree_desc));
+
+ auto const* action = g.ActionNodeWithId(make_tree_id);
+ REQUIRE_FALSE(action == nullptr);
+ auto const* bazel = g.ArtifactNodeWithId(bazel_desc.Id());
+ REQUIRE_FALSE(bazel == nullptr);
+
+ // run action
+ auto api = factory();
+ Executor runner{api.get(), ReadPlatformPropertiesFromEnv()};
+ CHECK_FALSE(runner.Process(action));
+ }
+ }
+}
+
+#endif // INCLUDED_SRC_TEST_BUILDTOOL_EXECUTION_ENGINE_EXECUTOR_EXECUTOR_API_TEST_HPP
diff --git a/test/buildtool/execution_engine/executor/executor_api_local.test.cpp b/test/buildtool/execution_engine/executor/executor_api_local.test.cpp
new file mode 100755
index 00000000..955e1682
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/executor_api_local.test.cpp
@@ -0,0 +1,36 @@
+#include "catch2/catch.hpp"
+#include "src/buildtool/execution_api/local/local_api.hpp"
+#include "src/buildtool/execution_api/remote/config.hpp"
+#include "src/buildtool/execution_engine/executor/executor.hpp"
+#include "test/buildtool/execution_engine/executor/executor_api.test.hpp"
+#include "test/utils/hermeticity/local.hpp"
+
+TEST_CASE_METHOD(HermeticLocalTestFixture,
+ "Executor<LocalApi>: Upload blob",
+ "[executor]") {
+ TestBlobUpload([&] { return std::make_unique<LocalApi>(); });
+}
+
+TEST_CASE_METHOD(HermeticLocalTestFixture,
+ "Executor<LocalApi>: Compile hello world",
+ "[executor]") {
+ TestHelloWorldCompilation([&] { return std::make_unique<LocalApi>(); });
+}
+
+TEST_CASE_METHOD(HermeticLocalTestFixture,
+ "Executor<LocalApi>: Compile greeter",
+ "[executor]") {
+ TestGreeterCompilation([&] { return std::make_unique<LocalApi>(); });
+}
+
+TEST_CASE_METHOD(HermeticLocalTestFixture,
+ "Executor<LocalApi>: Upload and download trees",
+ "[executor]") {
+ TestUploadAndDownloadTrees([&] { return std::make_unique<LocalApi>(); });
+}
+
+TEST_CASE_METHOD(HermeticLocalTestFixture,
+ "Executor<LocalApi>: Retrieve output directories",
+ "[executor]") {
+ TestRetrieveOutputDirectories([&] { return std::make_unique<LocalApi>(); });
+}
diff --git a/test/buildtool/execution_engine/executor/executor_api_remote_bazel.test.cpp b/test/buildtool/execution_engine/executor/executor_api_remote_bazel.test.cpp
new file mode 100755
index 00000000..d6ad57b1
--- /dev/null
+++ b/test/buildtool/execution_engine/executor/executor_api_remote_bazel.test.cpp
@@ -0,0 +1,71 @@
+#include "catch2/catch.hpp"
+#include "src/buildtool/execution_api/remote/bazel/bazel_api.hpp"
+#include "src/buildtool/execution_api/remote/config.hpp"
+#include "src/buildtool/execution_engine/executor/executor.hpp"
+#include "test/buildtool/execution_engine/executor/executor_api.test.hpp"
+
+TEST_CASE("Executor<BazelApi>: Upload blob", "[executor]") {
+ ExecutionConfiguration config;
+ auto const& info = RemoteExecutionConfig::Instance();
+
+ TestBlobUpload([&] {
+ return BazelApi::Ptr{
+ new BazelApi{"remote-execution", info.Host(), info.Port(), config}};
+ });
+}
+
+TEST_CASE("Executor<BazelApi>: Compile hello world", "[executor]") {
+ ExecutionConfiguration config;
+ config.skip_cache_lookup = false;
+
+ auto const& info = RemoteExecutionConfig::Instance();
+
+ TestHelloWorldCompilation(
+ [&] {
+ return BazelApi::Ptr{new BazelApi{
+ "remote-execution", info.Host(), info.Port(), config}};
+ },
+ false /* not hermetic */);
+}
+
+TEST_CASE("Executor<BazelApi>: Compile greeter", "[executor]") {
+ ExecutionConfiguration config;
+ config.skip_cache_lookup = false;
+
+ auto const& info = RemoteExecutionConfig::Instance();
+
+ TestGreeterCompilation(
+ [&] {
+ return BazelApi::Ptr{new BazelApi{
+ "remote-execution", info.Host(), info.Port(), config}};
+ },
+ false /* not hermetic */);
+}
+
+TEST_CASE("Executor<BazelApi>: Upload and download trees", "[executor]") {
+ ExecutionConfiguration config;
+ config.skip_cache_lookup = false;
+
+ auto const& info = RemoteExecutionConfig::Instance();
+
+ TestUploadAndDownloadTrees(
+ [&] {
+ return BazelApi::Ptr{new BazelApi{
+ "remote-execution", info.Host(), info.Port(), config}};
+ },
+ false /* not hermetic */);
+}
+
+TEST_CASE("Executor<BazelApi>: Retrieve output directories", "[executor]") {
+ ExecutionConfiguration config;
+ config.skip_cache_lookup = false;
+
+ auto const& info = RemoteExecutionConfig::Instance();
+
+ TestRetrieveOutputDirectories(
+ [&] {
+ return BazelApi::Ptr{new BazelApi{
+ "remote-execution", info.Host(), info.Port(), config}};
+ },
+ false /* not hermetic */);
+}