From 619def44c1cca9f3cdf63544d5f24f2c7a7d9b77 Mon Sep 17 00:00:00 2001 From: Klaus Aehlig Date: Tue, 22 Feb 2022 17:03:21 +0100 Subject: Initial self-hosting commit This is the initial version of our tool that is able to build itself. In can be bootstrapped by ./bin/bootstrap.py Co-authored-by: Oliver Reiche Co-authored-by: Victor Moreno --- .../execution_engine/executor/executor.test.cpp | 358 +++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100755 test/buildtool/execution_engine/executor/executor.test.cpp (limited to 'test/buildtool/execution_engine/executor/executor.test.cpp') 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 +#include +#include + +#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 outputs{}; + }; + + struct TestResponseConfig { + bool cached{}; + int exit_code{}; + }; + + std::unordered_map 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 const& /*unused*/, + std::vector const& /*unused*/, + std::vector const& /*unused*/, + std::map const& /*unused*/, + std::map const& /*unused*/) noexcept + -> IExecutionAction::Ptr final { + return IExecutionAction::Ptr{new TestAction(config_)}; + } + auto RetrieveToPaths( + std::vector const& /*unused*/, + std::vector const& /*unused*/) noexcept + -> bool final { + return false; // not needed by Executor + } + auto RetrieveToFds(std::vector const& /*unused*/, + std::vector 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 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 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))); + } +} -- cgit v1.2.3