object-introspection/test/integration/runner_common.cpp
2023-04-26 16:20:53 +01:00

420 lines
13 KiB
C++

#include "runner_common.h"
#include <toml++/toml.h>
#include <algorithm>
#include <boost/algorithm/string.hpp>
#include <boost/asio.hpp>
#include <boost/process.hpp>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <ios>
#include <iostream>
#include <string>
#include <utility>
#include "oi/OIOpts.h"
using namespace std::literals;
namespace bp = boost::process;
namespace bpt = boost::property_tree;
namespace fs = std::filesystem;
std::string oidExe = OID_EXE_PATH;
std::string configFile = CONFIG_FILE_PATH;
bool verbose = false;
bool preserve = false;
bool preserve_on_failure = false;
bool run_skipped_tests = false;
std::vector<std::string> extra_feature_args{};
constexpr static OIOpts opts{
OIOpt{'h', "help", no_argument, nullptr, "Print this message and exit"},
OIOpt{'p', "preserve", no_argument, nullptr,
"Do not clean up files generated by OID after tests are finished"},
OIOpt{'P', "preserve-on-failure", no_argument, nullptr,
"Do not clean up files generated by OID for failed tests"},
OIOpt{'v', "verbose", no_argument, nullptr,
"Verbose output. Show OID's stdout and stderr on test failure"},
OIOpt{'f', "force", no_argument, nullptr,
"Force running tests, even if they are marked as skipped"},
OIOpt{'x', "oid", required_argument, nullptr,
"Path to OID executable to test"},
OIOpt{'\0', "enable-feature", required_argument, nullptr,
"Enable extra OID feature."},
};
void usage(std::string_view progname) {
std::cout << "usage: " << progname << " ...\n";
std::cout << opts;
}
int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
int c;
while ((c = getopt_long(argc, argv, opts.shortOpts(), opts.longOpts(),
nullptr)) != -1) {
switch (c) {
case 'p':
preserve = true;
break;
case 'P':
preserve_on_failure = true;
break;
case 'v':
verbose = true;
break;
case 'f':
run_skipped_tests = true;
break;
case 'x':
// Must convert to absolute path so it continues to work after working
// directory is changed
oidExe = fs::absolute(optarg);
break;
case '\0':
extra_feature_args.push_back(std::string("-f") + optarg);
break;
case 'h':
default:
usage(argv[0]);
return 0;
}
}
return RUN_ALL_TESTS();
}
void IntegrationBase::SetUp() {
// Move into a temporary directory to run the test in an isolated environment
auto tmp_dir = TmpDirStr();
char* res = mkdtemp(&tmp_dir[0]);
if (!res)
abort();
workingDir = std::move(tmp_dir);
fs::current_path(workingDir);
}
void IntegrationBase::TearDown() {
const ::testing::TestInfo* const test_info =
::testing::UnitTest::GetInstance()->current_test_info();
if (preserve || (preserve_on_failure && test_info->result()->Failed())) {
std::cerr << "Working directory preserved at: " << workingDir << std::endl;
} else {
fs::remove_all(workingDir);
}
}
int IntegrationBase::exit_code(Proc& proc) {
proc.ctx.run();
proc.proc.wait();
// Read stdout and stderr into members, as required by certain tests (see
// gen_tests.py)
{
std::ifstream stdout(workingDir / "stdout");
stdout_.assign(std::istreambuf_iterator<char>(stdout),
std::istreambuf_iterator<char>());
}
{
std::ifstream stderr(workingDir / "stderr");
stderr_.assign(std::istreambuf_iterator<char>(stderr),
std::istreambuf_iterator<char>());
}
return proc.proc.exit_code();
}
fs::path IntegrationBase::createCustomConfig(const std::string& prefix,
const std::string& suffix) {
// If no extra config provided, return the config path unaltered.
if (prefix.empty() && suffix.empty()) {
return configFile;
}
auto customConfigFile = workingDir / "oid.config.toml";
auto config = toml::parse_file(configFile);
// As relative paths are allowed, we must canonicalise the paths before
// moving the file to the temporary directory.
fs::path configDirectory = fs::path(configFile).remove_filename();
if (toml::table* types = config["types"].as_table()) {
if (toml::array* arr = (*types)["containers"].as_array()) {
arr->for_each([&](auto&& el) {
if constexpr (toml::is_string<decltype(el)>) {
el = configDirectory / el.get();
}
});
}
}
if (toml::table* headers = config["headers"].as_table()) {
for (auto& path : {"user_paths", "system_paths"}) {
if (toml::array* arr = (*headers)[path].as_array()) {
arr->for_each([&](auto&& el) {
if constexpr (toml::is_string<decltype(el)>) {
el = configDirectory / el.get();
}
});
}
}
}
std::ofstream customConfig(customConfigFile, std::ios_base::app);
if (!prefix.empty()) {
customConfig << "\n\n# Test custom config start\n\n";
customConfig << prefix;
customConfig << "\n\n# Test custom config end\n\n";
}
customConfig << config;
if (!suffix.empty()) {
customConfig << "\n\n# Test custom config start\n\n";
customConfig << suffix;
customConfig << "\n\n# Test custom config end\n\n";
}
return customConfigFile;
}
std::string OidIntegration::TmpDirStr() {
return std::string("/tmp/oid-integration-XXXXXX");
}
OidProc OidIntegration::runOidOnProcess(OidOpts opts,
std::vector<std::string> extra_args,
std::string configPrefix,
std::string configSuffix) {
// Binary paths are populated by CMake
std::string targetExe =
std::string(TARGET_EXE_PATH) + " " + opts.targetArgs + " 1000";
/* Spawn the target process with all IOs redirected to /dev/null to not polute
* the terminal */
// clang-format off
bp::child targetProcess(
targetExe,
bp::std_in < bp::null,
bp::std_out > bp::null,
bp::std_err > bp::null,
opts.ctx);
// clang-format on
{
// Delete previous segconfig, in case oid re-used the PID
fs::path segconfigPath =
"/tmp/oid-segconfig-" + std::to_string(targetProcess.id());
fs::remove(segconfigPath);
// Create the segconfig, so we have the rights to delete it above
std::ofstream touch(segconfigPath);
}
fs::path thisConfig = createCustomConfig(configPrefix, configSuffix);
// Keep PID as the last argument to make it easier for users to directly copy
// and modify the command from the verbose mode output.
// clang-format off
auto default_args = std::array{
"--debug-level=3"s,
"--timeout=20"s,
"--dump-json"s,
"--config-file"s, thisConfig.string(),
"--script-source"s, opts.scriptSource,
"--pid"s, std::to_string(targetProcess.id()),
};
// clang-format on
// Specify feature args first so they can be overridden by extra_args of
// specific tests.
auto oid_args = extra_feature_args;
oid_args.insert(oid_args.end(), extra_args.begin(), extra_args.end());
oid_args.insert(oid_args.end(), default_args.begin(), default_args.end());
if (verbose) {
std::cerr << "Running: " << targetExe << "\n";
std::cerr << "Running: " << oidExe << " ";
for (const auto& arg : oid_args) {
std::cerr << arg << " ";
}
std::cerr << std::endl;
}
// Use tee to write the output to files. If verbose is on, also redirect the
// output to stderr.
bp::async_pipe std_out_pipe(opts.ctx), std_err_pipe(opts.ctx);
bp::child std_out, std_err;
if (verbose) {
// clang-format off
std_out = bp::child(bp::search_path("tee"),
(workingDir / "stdout").string(),
bp::std_in < std_out_pipe,
bp::std_out > stderr,
opts.ctx);
std_err = bp::child(bp::search_path("tee"),
(workingDir / "stderr").string(),
bp::std_in < std_err_pipe,
bp::std_out > stderr,
opts.ctx);
// clang-format on
} else {
// clang-format off
std_out = bp::child(bp::search_path("tee"),
(workingDir / "stdout").string(),
bp::std_in < std_out_pipe,
bp::std_out > bp::null,
opts.ctx);
std_err = bp::child(bp::search_path("tee"),
(workingDir / "stderr").string(),
bp::std_in < std_err_pipe,
bp::std_out > bp::null,
opts.ctx);
// clang-format on
}
/* Spawn `oid` with tracing on and IOs redirected */
// clang-format off
bp::child oidProcess(
oidExe,
bp::args(oid_args),
bp::env["OID_METRICS_TRACE"] = "time",
bp::std_in < bp::null,
bp::std_out > std_out_pipe,
bp::std_err > std_err_pipe,
opts.ctx);
// clang-format on
return OidProc{
.target = Proc{opts.ctx, std::move(targetProcess), {}, {}},
.oid = Proc{opts.ctx, std::move(oidProcess), std::move(std_out),
std::move(std_err)},
};
}
void OidIntegration::compare_json(const bpt::ptree& expected_json,
const bpt::ptree& actual_json,
const std::string& full_key,
bool expect_eq) {
if (expected_json.empty()) {
if (expect_eq) {
ASSERT_EQ(expected_json.data(), actual_json.data())
<< "Incorrect value for key: " << full_key;
} else {
ASSERT_NE(expected_json.data(), actual_json.data())
<< "Incorrect value for key: " << full_key;
}
}
if (expected_json.begin()->first == "") {
// Empty key - assume this is an array
ASSERT_EQ(expected_json.size(), actual_json.size())
<< "Array size difference for key: " << full_key;
int i = 0;
for (auto e_it = expected_json.begin(), a_it = actual_json.begin();
e_it != expected_json.end() && a_it != actual_json.end();
e_it++, a_it++) {
compare_json(e_it->second, a_it->second,
full_key + "[" + std::to_string(i) + "]", expect_eq);
i++;
}
return;
}
// Compare as Key-Value pairs
for (const auto& [key, val] : expected_json) {
if (key == "NOT") {
auto curr_key = full_key + ".NOT";
ASSERT_EQ(true, expect_eq) << "Invalid expected data: " << curr_key
<< " - Can not use nested \"NOT\" expressions";
if (val.empty()) {
// Check that a given single key does not exist
const auto& key_to_check = val.data();
auto actual_it = actual_json.find(key_to_check);
auto curr_key = full_key + "." + key_to_check;
if (actual_it != actual_json.not_found()) {
ADD_FAILURE() << "Unexpected key found in output: " << curr_key;
}
} else {
// Check that a key's value is not equal to something
compare_json(val, actual_json, curr_key, !expect_eq);
}
continue;
}
auto actual_it = actual_json.find(key);
auto curr_key = full_key + "." + key;
if (actual_it == actual_json.not_found()) {
ADD_FAILURE() << "Expected key not found in output: " << curr_key;
continue;
}
compare_json(val, actual_it->second, curr_key, expect_eq);
}
}
std::string OilIntegration::TmpDirStr() {
return std::string("/tmp/oil-integration-XXXXXX");
}
Proc OilIntegration::runOilTarget(OidOpts opts,
std::string configPrefix,
std::string configSuffix) {
fs::path thisConfig = createCustomConfig(configPrefix, configSuffix);
std::string targetExe = std::string(TARGET_EXE_PATH) + " " + opts.targetArgs +
" " + thisConfig.string();
if (verbose) {
std::cerr << "Running: " << targetExe << std::endl;
}
// Use tee to write the output to files. If verbose is on, also redirect the
// output to stderr.
bp::async_pipe std_out_pipe(opts.ctx), std_err_pipe(opts.ctx);
bp::child std_out, std_err;
if (verbose) {
// clang-format off
std_out = bp::child(bp::search_path("tee"),
(workingDir / "stdout").string(),
bp::std_in < std_out_pipe,
bp::std_out > stderr,
opts.ctx);
std_err = bp::child(bp::search_path("tee"),
(workingDir / "stderr").string(),
bp::std_in < std_err_pipe,
bp::std_out > stderr,
opts.ctx);
// clang-format on
} else {
// clang-format off
std_out = bp::child(bp::search_path("tee"),
(workingDir / "stdout").string(),
bp::std_in < std_out_pipe,
bp::std_out > bp::null,
opts.ctx);
std_err = bp::child(bp::search_path("tee"),
(workingDir / "stderr").string(),
bp::std_in < std_err_pipe,
bp::std_out > bp::null,
opts.ctx);
// clang-format on
}
/* Spawn target with tracing on and IOs redirected in custom pipes to be read
* later */
// clang-format off
bp::child targetProcess(
targetExe,
bp::std_in < bp::null,
bp::std_out > std_out_pipe,
bp::std_err > std_err_pipe,
opts.ctx);
// clang-format on
return Proc{opts.ctx, std::move(targetProcess), std::move(std_out),
std::move(std_err)};
}