}; | }; | ||||
void log_failure(const test_failure& fail) { | void log_failure(const test_failure& fail) { | ||||
spdlog::error("Test '{}' failed! [exitted {}]", fail.executable_path.string(), fail.retc); | |||||
spdlog::error("Test '{}' failed! [exited {}]", fail.executable_path.string(), fail.retc); | |||||
if (fail.signal) { | if (fail.signal) { | ||||
spdlog::error("Test execution received signal {}", fail.signal); | spdlog::error("Test execution received signal {}", fail.signal); | ||||
} | } |
auto exe_path = calc_executable_path(env); | auto exe_path = calc_executable_path(env); | ||||
auto msg = fmt::format("Run test: {:30}", fs::relative(exe_path, env.output_root).string()); | auto msg = fmt::format("Run test: {:30}", fs::relative(exe_path, env.output_root).string()); | ||||
spdlog::info(msg); | spdlog::info(msg); | ||||
auto&& [dur, res] | |||||
= timed<std::chrono::microseconds>([&] { return run_proc({exe_path.string()}); }); | |||||
using namespace std::chrono_literals; | |||||
auto&& [dur, res] = timed<std::chrono::microseconds>( | |||||
[&] { return run_proc({.command = {exe_path.string()}, .timeout = 10s}); }); | |||||
if (res.okay()) { | if (res.okay()) { | ||||
spdlog::info("{} - PASSED - {:>9n}μs", msg, dur.count()); | spdlog::info("{} - PASSED - {:>9n}μs", msg, dur.count()); | ||||
return std::nullopt; | return std::nullopt; | ||||
} else { | } else { | ||||
spdlog::error("{} - FAILED - {:>9n}μs [exited {}]", msg, dur.count(), res.retc); | |||||
auto exit_msg = fmt::format(res.signal ? "signalled {}" : "exited {}", | |||||
res.signal ? res.signal : res.retc); | |||||
auto fail_str = res.timed_out ? "TIMEOUT" : "FAILED "; | |||||
spdlog::error("{} - {} - {:>9n}μs [{}]", msg, fail_str, dur.count(), exit_msg); | |||||
test_failure f; | test_failure f; | ||||
f.executable_path = exe_path; | f.executable_path = exe_path; | ||||
f.output = res.output; | f.output = res.output; |
#pragma once | #pragma once | ||||
#include <chrono> | |||||
#include <optional> | |||||
#include <string> | #include <string> | ||||
#include <string_view> | #include <string_view> | ||||
#include <vector> | #include <vector> | ||||
struct proc_result { | struct proc_result { | ||||
int signal = 0; | int signal = 0; | ||||
int retc = 0; | int retc = 0; | ||||
bool timed_out = false; | |||||
std::string output; | std::string output; | ||||
bool okay() const noexcept { return retc == 0 && signal == 0; } | bool okay() const noexcept { return retc == 0 && signal == 0; } | ||||
}; | }; | ||||
proc_result run_proc(const std::vector<std::string>& args); | |||||
struct proc_options { | |||||
std::vector<std::string> command; | |||||
/** | |||||
* Timeout for the subprocess, in milliseconds. If zero, will wait forever | |||||
*/ | |||||
std::optional<std::chrono::milliseconds> timeout = std::nullopt; | |||||
}; | |||||
proc_result run_proc(const proc_options& opts); | |||||
inline proc_result run_proc(std::vector<std::string> args) { | |||||
return run_proc(proc_options{.command = std::move(args)}); | |||||
} | |||||
} // namespace dds | } // namespace dds |
#include <spdlog/spdlog.h> | #include <spdlog/spdlog.h> | ||||
#include <poll.h> | #include <poll.h> | ||||
#include <signal.h> | |||||
#include <sys/wait.h> | #include <sys/wait.h> | ||||
#include <unistd.h> | #include <unistd.h> | ||||
} // namespace | } // namespace | ||||
proc_result dds::run_proc(const std::vector<std::string>& command) { | |||||
spdlog::debug("Spawning subprocess: {}", quote_command(command)); | |||||
proc_result dds::run_proc(const proc_options& opts) { | |||||
spdlog::debug("Spawning subprocess: {}", quote_command(opts.command)); | |||||
int stdio_pipe[2] = {}; | int stdio_pipe[2] = {}; | ||||
auto rc = ::pipe(stdio_pipe); | auto rc = ::pipe(stdio_pipe); | ||||
check_rc(rc == 0, "Create stdio pipe for subprocess"); | check_rc(rc == 0, "Create stdio pipe for subprocess"); | ||||
int read_pipe = stdio_pipe[0]; | int read_pipe = stdio_pipe[0]; | ||||
int write_pipe = stdio_pipe[1]; | int write_pipe = stdio_pipe[1]; | ||||
auto child = spawn_child(command, write_pipe, read_pipe); | |||||
auto child = spawn_child(opts.command, write_pipe, read_pipe); | |||||
::close(write_pipe); | ::close(write_pipe); | ||||
proc_result res; | proc_result res; | ||||
using namespace std::chrono_literals; | |||||
auto timeout = opts.timeout; | |||||
while (true) { | while (true) { | ||||
rc = ::poll(&stdio_fd, 1, -1); | |||||
rc = ::poll(&stdio_fd, 1, static_cast<int>(timeout.value_or(-1ms).count())); | |||||
if (rc && errno == EINTR) { | if (rc && errno == EINTR) { | ||||
errno = 0; | errno = 0; | ||||
continue; | continue; | ||||
} | } | ||||
check_rc(rc > 0, "Failed in poll()"); | |||||
if (rc == 0) { | |||||
// Timeout! | |||||
::kill(child, SIGINT); | |||||
timeout = std::nullopt; | |||||
res.timed_out = true; | |||||
spdlog::debug("Subprocess [{}] timed out", quote_command(opts.command)); | |||||
continue; | |||||
} | |||||
std::string buffer; | std::string buffer; | ||||
buffer.resize(1024); | buffer.resize(1024); | ||||
auto nread = ::read(stdio_fd.fd, buffer.data(), buffer.size()); | auto nread = ::read(stdio_fd.fd, buffer.data(), buffer.size()); |
#ifdef _WIN32 | #ifdef _WIN32 | ||||
#include "./proc.hpp" | #include "./proc.hpp" | ||||
#include <neo/assert.hpp> | |||||
#include <spdlog/spdlog.h> | #include <spdlog/spdlog.h> | ||||
#include <wil/resource.h> | #include <wil/resource.h> | ||||
#include <stdexcept> | #include <stdexcept> | ||||
using namespace dds; | using namespace dds; | ||||
using namespace std::chrono_literals; | |||||
namespace { | namespace { | ||||
} // namespace | } // namespace | ||||
proc_result dds::run_proc(const std::vector<std::string>& cmd) { | |||||
auto cmd_str = quote_command(cmd); | |||||
proc_result dds::run_proc(const proc_options& opts) { | |||||
auto cmd_str = quote_command(opts.command); | |||||
::SECURITY_ATTRIBUTES security = {}; | ::SECURITY_ATTRIBUTES security = {}; | ||||
security.bInheritHandle = TRUE; | security.bInheritHandle = TRUE; | ||||
} | } | ||||
::SetHandleInformation(reader.get(), HANDLE_FLAG_INHERIT, 0); | ::SetHandleInformation(reader.get(), HANDLE_FLAG_INHERIT, 0); | ||||
::COMMTIMEOUTS timeouts; | |||||
::GetCommTimeouts(reader.get(), &timeouts); | |||||
wil::unique_process_information proc_info; | wil::unique_process_information proc_info; | ||||
nullptr, | nullptr, | ||||
nullptr, | nullptr, | ||||
true, | true, | ||||
0, | |||||
CREATE_NEW_PROCESS_GROUP, | |||||
nullptr, | nullptr, | ||||
nullptr, | nullptr, | ||||
&startup_info, | &startup_info, | ||||
writer.reset(); | writer.reset(); | ||||
std::string output; | std::string output; | ||||
proc_result res; | |||||
auto timeout = opts.timeout; | |||||
while (true) { | while (true) { | ||||
const int buffer_size = 256; | const int buffer_size = 256; | ||||
char buffer[buffer_size]; | char buffer[buffer_size]; | ||||
DWORD nread = 0; | DWORD nread = 0; | ||||
okay = ::ReadFile(reader.get(), buffer, buffer_size, &nread, nullptr); | |||||
// Reload the timeout on the pipe | |||||
timeouts.ReadTotalTimeoutConstant = static_cast<DWORD>(timeout.value_or(0ms).count()); | |||||
::SetCommTimeouts(reader.get(), &timeouts); | |||||
// Read some bytes from the process | |||||
okay = ::ReadFile(reader.get(), buffer, buffer_size, &nread, nullptr); | |||||
if (!okay && ::GetLastError() == ERROR_TIMEOUT) { | |||||
// We didn't read any bytes. Hit the timeout | |||||
neo_assert_always(invariant, | |||||
nread == 0, | |||||
"Didn't expect to read bytes when a timeout was reached", | |||||
nread, | |||||
timeout->count()); | |||||
res.timed_out = true; | |||||
timeout = std::nullopt; | |||||
::GenerateConsoleCtrlEvent(CTRL_C_EVENT, proc_info.dwProcessId); | |||||
continue; | |||||
} | |||||
if (!okay && ::GetLastError() != ERROR_BROKEN_PIPE) { | if (!okay && ::GetLastError() != ERROR_BROKEN_PIPE) { | ||||
throw_system_error("Failed while reading from the stdio pipe"); | throw_system_error("Failed while reading from the stdio pipe"); | ||||
} | } | ||||
throw_system_error("Failed reading exit code of process"); | throw_system_error("Failed reading exit code of process"); | ||||
} | } | ||||
proc_result res; | |||||
res.retc = rc; | res.retc = rc; | ||||
res.output = std::move(output); | res.output = std::move(output); | ||||
return res; | return res; |
"cxx_compiler": "g++-9", | "cxx_compiler": "g++-9", | ||||
"flags": [ | "flags": [ | ||||
"-Werror=return-type", | "-Werror=return-type", | ||||
"-fsanitize=address", | |||||
// "-fsanitize=address", | |||||
], | ], | ||||
"cxx_flags": [ | "cxx_flags": [ | ||||
"-fconcepts", | "-fconcepts", | ||||
"-std=c++2a", | "-std=c++2a", | ||||
], | ], | ||||
"link_flags": [ | "link_flags": [ | ||||
// "-static-libgcc", | |||||
// "-static-libstdc++" | |||||
"-fsanitize=address", | |||||
"-fuse-ld=lld", | |||||
"-static-libgcc", | |||||
"-static-libstdc++" | |||||
// "-fsanitize=address", | |||||
// "-fuse-ld=lld", | |||||
], | ], | ||||
"debug": true, | "debug": true, | ||||
// "optimize": true, | // "optimize": true, |