diff options
-rw-r--r-- | test/utils/TARGETS | 17 | ||||
-rw-r--r-- | test/utils/serve_service/RULES | 215 | ||||
-rw-r--r-- | test/utils/serve_service/TARGETS | 1 | ||||
-rw-r--r-- | test/utils/serve_service/main-serve.cpp | 132 | ||||
-rwxr-xr-x | test/utils/serve_service/test_runner.py | 139 | ||||
-rw-r--r-- | test/utils/test_env.hpp | 22 |
6 files changed, 526 insertions, 0 deletions
diff --git a/test/utils/TARGETS b/test/utils/TARGETS index c8fe2f19..2a6f99d0 100644 --- a/test/utils/TARGETS +++ b/test/utils/TARGETS @@ -61,6 +61,23 @@ ] , "stage": ["test", "utils"] } +, "catch-main-serve": + { "type": ["@", "rules", "CC", "library"] + , "name": ["catch-main-serve"] + , "srcs": ["serve_service/main-serve.cpp"] + , "deps": + [ ["@", "catch2", "", "catch2"] + , ["@", "src", "src/buildtool/execution_api/remote", "config"] + , ["@", "src", "src/buildtool/serve_api/remote", "config"] + , ["@", "src", "src/buildtool/storage", "storage"] + , ["@", "src", "src/buildtool/file_system", "file_system_manager"] + , ["@", "src", "src/buildtool/compatibility", "compatibility"] + , "shell_quoting" + , "log_config" + , "test_env" + ] + , "stage": ["test", "utils"] + } , "test_utils_install": { "type": "install" , "tainted": ["test"] diff --git a/test/utils/serve_service/RULES b/test/utils/serve_service/RULES new file mode 100644 index 00000000..a59aa974 --- /dev/null +++ b/test/utils/serve_service/RULES @@ -0,0 +1,215 @@ +{ "CC test": + { "doc": + ["A test written in C++, depending on serve service and remote execution"] + , "tainted": ["test"] + , "target_fields": ["srcs", "private-hdrs", "private-deps", "data"] + , "string_fields": + [ "name" + , "args" + , "stage" + , "pure C" + , "private-defines" + , "private-cflags" + , "private-ldflags" + , "compatible remote" + ] + , "config_vars": + [ "ARCH" + , "HOST_ARCH" + , "CC" + , "CXX" + , "CFLAGS" + , "CXXFLAGS" + , "ADD_CFLAGS" + , "ADD_CXXFLAGS" + , "ENV" + , "TEST_ENV" + , "CC_TEST_LAUNCHER" + , "TEST_COMPATIBLE_REMOTE" + ] + , "implicit": + { "defaults": [["@", "rules", "CC", "defaults"]] + , "runner": ["test_runner.py"] + , "just": [["@", "src", "", "installed just"]] + } + , "field_doc": + { "name": + [ "The name of the test" + , "" + , "Used to name the test binary as well as for staging the test result" + ] + , "args": ["Command line arguments for the test binary"] + , "srcs": ["The sources of the test binary"] + , "private-hdrs": + [ "Any additional header files that need to be present when compiling" + , "the test binary." + ] + , "private-defines": + [ "List of defines set for source files local to this target." + , "Each list entry will be prepended by \"-D\"." + ] + , "private-cflags": + ["List of compile flags set for source files local to this target."] + , "private-ldflags": + [ "Additional linker flags for linking external libraries (not built" + , "by this tool, typically system libraries)." + ] + , "stage": + [ "The logical location of all header and source files." + , "Individual directory components are joined with \"/\"." + ] + , "pure C": + [ "If non-empty, compile as C sources rathter than C++ sources." + , "In particular, CC is used to compile rather than CXX (or their" + , "respective defaults)." + ] + , "data": ["Any files the test binary needs access to when running"] + } + , "config_doc": + { "CC": ["The name of the C compiler to be used."] + , "CXX": ["The name of the C++ compiler to be used."] + , "CFLAGS": + [ "The flags for CC to be used instead of the default ones" + , "taken from the [\"CC\", \"defaults\"] target" + ] + , "CXXFLAGS": + [ "The flags for CXX to be used instead of the default ones" + , "taken from the [\"CC\", \"defaults\"] target" + ] + , "ADD_CFLAGS": + [ "The flags to add to the default ones for CC" + , "taken from the [\"CC\", \"defaults\"] target" + ] + , "ADD_CXXFLAGS": + [ "The flags to add to the default ones for CXX" + , "taken from the [\"CC\", \"defaults\"] target" + ] + , "ENV": ["The environment for any action generated."] + , "TEST_ENV": ["The environment for executing the test runner."] + , "CC_TEST_LAUNCHER": + [ "List of strings representing the launcher that is prepend to the" + , "command line for running the test binary." + ] + , "TEST_COMPATIBLE_REMOTE": + [ "If true, provide compatible remote execution and set COMPATIBLE in the" + , " test environment; otherwise, provide a native remote execution" + , "do not modify the environment" + ] + } + , "artifacts_doc": + [ "result: the result of this test (\"PASS\" or \"FAIL\"); useful for" + , " generating test reports." + , "stdout/stderr: Any output the invocation of the test binary produced on" + , " the respective file descriptor" + , "time-start/time-stop: The time (decimally coded) in seconds since the" + , " epoch when the test invocation started and ended." + ] + , "runfiles_doc": + [ "A tree consisting of the artifacts staged at the name of the test." + , "As the built-in \"install\" rule only takes the runfiles of its" + , "\"private-deps\" argument, this gives an easy way of defining test" + , "suites." + ] + , "imports": + { "artifacts": ["@", "rules", "", "field_artifacts"] + , "runfiles": ["@", "rules", "", "field_runfiles"] + , "host transition": ["@", "rules", "transitions", "for host"] + , "stage": ["@", "rules", "", "stage_singleton_field"] + , "run_test": ["@", "rules", "CC/test", "run_test"] + } + , "config_transitions": + { "defaults": [{"type": "CALL_EXPRESSION", "name": "host transition"}] + , "private-deps": [{"type": "CALL_EXPRESSION", "name": "host transition"}] + , "data": [{"type": "CALL_EXPRESSION", "name": "host transition"}] + , "just": [{"type": "CALL_EXPRESSION", "name": "host transition"}] + , "runner": [{"type": "CALL_EXPRESSION", "name": "host transition"}] + } + , "expression": + { "type": "let*" + , "bindings": + [ [ "name" + , { "type": "assert_non_empty" + , "msg": "A non-empty name has to be provided for binaries" + , "$1": {"type": "join", "$1": {"type": "FIELD", "name": "name"}} + } + ] + , ["pure C", {"type": "FIELD", "name": "pure C"}] + , [ "stage" + , { "type": "join" + , "separator": "/" + , "$1": {"type": "FIELD", "name": "stage"} + } + ] + , [ "srcs" + , { "type": "to_subdir" + , "subdir": {"type": "var", "name": "stage"} + , "$1": + { "type": "let*" + , "bindings": [["fieldname", "srcs"]] + , "body": {"type": "CALL_EXPRESSION", "name": "artifacts"} + } + } + ] + , [ "private-hdrs" + , { "type": "to_subdir" + , "subdir": {"type": "var", "name": "stage"} + , "$1": + { "type": "let*" + , "bindings": [["fieldname", "private-hdrs"]] + , "body": {"type": "CALL_EXPRESSION", "name": "artifacts"} + } + } + ] + , ["host-trans", {"type": "CALL_EXPRESSION", "name": "host transition"}] + , ["defaults-transition", {"type": "var", "name": "host-trans"}] + , ["deps-transition", {"type": "var", "name": "host-trans"}] + , ["deps-fieldnames", ["private-deps", "defaults"]] + , ["transition", {"type": "CALL_EXPRESSION", "name": "host transition"}] + , ["fieldname", "runner"] + , ["location", "runner"] + , ["runner", {"type": "CALL_EXPRESSION", "name": "stage"}] + , ["fieldname", "just"] + , ["just", {"type": "CALL_EXPRESSION", "name": "artifacts"}] + , [ "compatible-remote" + , { "type": "singleton_map" + , "key": "compatible-remote.json" + , "value": + { "type": "BLOB" + , "data": + { "type": "if" + , "cond": {"type": "var", "name": "TEST_COMPATIBLE_REMOTE"} + , "then": "true" + , "else": "false" + } + } + } + ] + , [ "runner-data" + , { "type": "map_union" + , "$1": + [ {"type": "var", "name": "compatible-remote"} + , {"type": "var", "name": "just"} + ] + } + ] + , ["test-args", {"type": "FIELD", "name": "args", "default": []}] + , [ "test-data" + , { "type": "let*" + , "bindings": + [ ["fieldname", "data"] + , ["transition", {"type": "var", "name": "deps-transition"}] + ] + , "body": + { "type": "map_union" + , "$1": + [ {"type": "CALL_EXPRESSION", "name": "runfiles"} + , {"type": "CALL_EXPRESSION", "name": "artifacts"} + ] + } + } + ] + ] + , "body": {"type": "CALL_EXPRESSION", "name": "run_test"} + } + } +} diff --git a/test/utils/serve_service/TARGETS b/test/utils/serve_service/TARGETS new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/utils/serve_service/TARGETS @@ -0,0 +1 @@ +{} diff --git a/test/utils/serve_service/main-serve.cpp b/test/utils/serve_service/main-serve.cpp new file mode 100644 index 00000000..0a559c0b --- /dev/null +++ b/test/utils/serve_service/main-serve.cpp @@ -0,0 +1,132 @@ +// Copyright 2023 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. + +#define CATCH_CONFIG_RUNNER +#include <chrono> +#include <cstdlib> +#include <filesystem> +#include <iostream> +#include <sstream> +#include <thread> + +#include "catch2/catch_session.hpp" +#include "catch2/catch_test_macros.hpp" +#include "src/buildtool/compatibility/compatibility.hpp" +#include "src/buildtool/execution_api/remote/config.hpp" +#include "src/buildtool/file_system/file_system_manager.hpp" +#include "src/buildtool/serve_api/remote/config.hpp" +#include "src/buildtool/storage/storage.hpp" +#include "test/utils/logging/log_config.hpp" +#include "test/utils/shell_quoting.hpp" +#include "test/utils/test_env.hpp" + +namespace { + +auto const kBundlePath = + std::string{"test/buildtool/file_system/data/test_repo.bundle"}; +auto const kBundlePathSymlinks = + std::string{"test/buildtool/file_system/data/test_repo_symlinks.bundle"}; + +void wait_for_grpc_to_shutdown() { + // grpc_shutdown_blocking(); // not working + std::this_thread::sleep_for(std::chrono::seconds(1)); +} + +[[nodiscard]] auto CloneRepo(std::filesystem::path const& repo_path, + std::string const& bundle, + bool is_bare = false) noexcept -> bool { + auto cmd = fmt::format("git clone {}{} {}", + is_bare ? "--bare " : "", + QuoteForShell(bundle), + QuoteForShell(repo_path.string())); + return (std::system(cmd.c_str()) == 0); +} + +[[nodiscard]] auto CreateServeTestRepo(std::filesystem::path const& repo_path, + std::string const& bundle, + bool is_bare = false) noexcept -> bool { + if (not CloneRepo(repo_path, bundle, is_bare)) { + return false; + } + auto cmd = + fmt::format("git --git-dir={} --work-tree={} checkout master", + QuoteForShell(is_bare ? repo_path.string() + : (repo_path / ".git").string()), + QuoteForShell(repo_path.string())); + return (std::system(cmd.c_str()) == 0); +} + +/// \brief Configure serve service from test environment. In case the +/// environment variable is malformed, we write a message and stop execution. +/// The availability of the serve service known repositories (from known test +/// bundles) is also ensured. +/// \returns true If serve service was successfully configured. +[[nodiscard]] auto ConfigureServeService() -> bool { + // just serve shares here compatibility and authentication args with + // remote execution, so no need to do those again + auto address = ReadRemoteServeAddressFromEnv(); + if (address and not RemoteServeConfig::SetRemoteAddress(*address)) { + Logger::Log(LogLevel::Error, "parsing address '{}' failed.", *address); + std::exit(EXIT_FAILURE); + } + + auto repos = ReadRemoteServeReposFromEnv(); + if (not repos.empty() and + not RemoteServeConfig::SetKnownRepositories(repos)) { + Logger::Log(LogLevel::Error, "setting serve repos failed."); + std::exit(EXIT_FAILURE); + } + + // now actually populate the serve repositories, one bare and one non-bare + if (repos.size() != 2) { + Logger::Log(LogLevel::Error, + "Expected 2 serve repositories in test env."); + std::exit(EXIT_FAILURE); + } + if (not CreateServeTestRepo(repos[0], kBundlePath, /*is_bare=*/true) or + not CreateServeTestRepo( + repos[1], kBundlePathSymlinks, /*is_bare=*/false)) { + Logger::Log(LogLevel::Error, + "Failed to setup serve service repositories."); + std::exit(EXIT_FAILURE); + } + + return static_cast<bool>(RemoteServeConfig::RemoteAddress()); +} + +} // namespace + +auto main(int argc, char* argv[]) -> int { + ConfigureLogging(); + + // Setup of serve service, including known repositories. + if (not ConfigureServeService()) { + return EXIT_FAILURE; + } + + /** + * The current implementation of libgit2 uses pthread_key_t incorrectly + * on POSIX systems to handle thread-specific data, which requires us to + * explicitly make sure the main thread is the first one to call + * git_libgit2_init. Future versions of libgit2 will hopefully fix this. + */ + GitContext::Create(); + + int result = Catch::Session().run(argc, argv); + + // valgrind fails if we terminate before grpc's async shutdown threads exit + wait_for_grpc_to_shutdown(); + + return result; +} diff --git a/test/utils/serve_service/test_runner.py b/test/utils/serve_service/test_runner.py new file mode 100755 index 00000000..08681646 --- /dev/null +++ b/test/utils/serve_service/test_runner.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# Copyright 2023 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. + +import json +import os +import subprocess +import time + +time_start: float = time.time() +time_stop: float = 0 +result: str = "UNKNOWN" +stderr: str = "" +stdout: str = "" + + +def dump_results() -> None: + with open("result", "w") as f: + f.write("%s\n" % (result, )) + with open("time-start", "w") as f: + f.write("%d\n" % (time_start, )) + with open("time-stop", "w") as f: + f.write("%d\n" % (time_stop, )) + with open("stdout", "w") as f: + f.write("%s\n" % (stdout, )) + with open("stderr", "w") as f: + f.write("%s\n" % (stderr, )) + + +dump_results() + +TEMP_DIR = os.path.realpath("scratch") +os.makedirs(TEMP_DIR, exist_ok=True) + +WORK_DIR = os.path.realpath("work") +os.makedirs(WORK_DIR, exist_ok=True) + +REMOTE_DIR = os.path.realpath("remote") +os.makedirs(REMOTE_DIR, exist_ok=True) + +SERVE_CONFIG_FILE = os.path.realpath("just-servec") + +# Set known serve repository roots under TEST_TMPDIR +TEST_SERVE_REPO_1 = os.path.join(TEMP_DIR, "test_serve_repo_1") +if os.path.exists(TEST_SERVE_REPO_1): + os.remove(TEST_SERVE_REPO_1) +TEST_SERVE_REPO_2 = os.path.join(TEMP_DIR, "test_serve_repo_2") +if os.path.exists(TEST_SERVE_REPO_2): + os.remove(TEST_SERVE_REPO_2) + +SERVE_REPOSITORIES = ";".join([TEST_SERVE_REPO_1, TEST_SERVE_REPO_2]) + +REMOTE_SERVE_INFO = os.path.join(REMOTE_DIR, "info_serve.json") + +if os.path.exists(REMOTE_SERVE_INFO): + print(f"Warning: removing unexpected info file {REMOTE_SERVE_INFO}") + os.remove(REMOTE_SERVE_INFO) + +# Run 'just serve' + +with open(SERVE_CONFIG_FILE, "w") as f: + f.write( + json.dumps({ + "repositories": [TEST_SERVE_REPO_1, TEST_SERVE_REPO_2], + "logging": { + "limit": 6, + "plain": True + }, + "remote service": { + "info file": REMOTE_SERVE_INFO + } + })) + +serve_cmd = ["./bin/just", "serve", SERVE_CONFIG_FILE] + +servestdout = open("servestdout", "w") +servestderr = open("servestderr", "w") +serve_proc = subprocess.Popen( + serve_cmd, + stdout=servestdout, + stderr=servestderr, +) + +while not os.path.exists(REMOTE_SERVE_INFO): + time.sleep(1) + +with open(REMOTE_SERVE_INFO) as f: + serve_info = json.load(f) + +REMOTE_SERVE_ADDRESS = "%s:%d" % (serve_info["interface"], serve_info["port"]) + +# Setup environment + +ENV = dict(os.environ, + TEST_TMPDIR=TEMP_DIR, + TMPDIR=TEMP_DIR, + REMOTE_SERVE_ADDRESS=REMOTE_SERVE_ADDRESS, + SERVE_REPOSITORIES=SERVE_REPOSITORIES) + +for k in ["TLS_CA_CERT", "TLS_CLIENT_CERT", "TLS_CLIENT_KEY"]: + if k in ENV: + del ENV[k] + +with open('test-launcher.json') as f: + test_launcher = json.load(f) + +with open('test-args.json') as f: + test_args = json.load(f) + +# Run the test + +time_start = time.time() + +ret = subprocess.run(test_launcher + ["../test"] + test_args, + cwd=WORK_DIR, + env=ENV, + capture_output=True) + +time_stop = time.time() +result = "PASS" if ret.returncode == 0 else "FAIL" +stdout = ret.stdout.decode("utf-8") +stderr = ret.stderr.decode("utf-8") +serve_proc.terminate() +sout, serr = serve_proc.communicate() + +dump_results() + +if result != "PASS": exit(1) diff --git a/test/utils/test_env.hpp b/test/utils/test_env.hpp index 7013d2ff..47396db2 100644 --- a/test/utils/test_env.hpp +++ b/test/utils/test_env.hpp @@ -54,6 +54,14 @@ static inline void ReadCompatibilityFromEnv() { : std::make_optional(std::string{execution_address}); } +[[nodiscard]] static inline auto ReadRemoteServeAddressFromEnv() + -> std::optional<std::string> { + auto* serve_address = std::getenv("REMOTE_SERVE_ADDRESS"); + return serve_address == nullptr + ? std::nullopt + : std::make_optional(std::string{serve_address}); +} + [[nodiscard]] static inline auto ReadTLSAuthArgsFromEnv() -> bool { auto* ca_cert = std::getenv("TLS_CA_CERT"); auto* client_cert = std::getenv("TLS_CLIENT_CERT"); @@ -81,4 +89,18 @@ static inline void ReadCompatibilityFromEnv() { return true; } +[[nodiscard]] static inline auto ReadRemoteServeReposFromEnv() + -> std::vector<std::filesystem::path> { + std::vector<std::filesystem::path> repos{}; + auto* serve_repos = std::getenv("SERVE_REPOSITORIES"); + if (serve_repos not_eq nullptr) { + std::istringstream pss(std::string{serve_repos}); + std::string path; + while (std::getline(pss, path, ';')) { + repos.emplace_back(path); + } + } + return repos; +} + #endif // INCLUDED_SRC_TEST_UTILS_TEST_ENV_HPP |