summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--test/utils/TARGETS17
-rw-r--r--test/utils/serve_service/RULES215
-rw-r--r--test/utils/serve_service/TARGETS1
-rw-r--r--test/utils/serve_service/main-serve.cpp132
-rwxr-xr-xtest/utils/serve_service/test_runner.py139
-rw-r--r--test/utils/test_env.hpp22
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