@@ -24,7 +24,7 @@ struct state { | |||
}; | |||
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) { | |||
spdlog::error("Test execution received signal {}", fail.signal); | |||
} |
@@ -78,13 +78,18 @@ std::optional<test_failure> link_executable_plan::run_test(build_env_ref env) co | |||
auto exe_path = calc_executable_path(env); | |||
auto msg = fmt::format("Run test: {:30}", fs::relative(exe_path, env.output_root).string()); | |||
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()) { | |||
spdlog::info("{} - PASSED - {:>9n}μs", msg, dur.count()); | |||
return std::nullopt; | |||
} 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; | |||
f.executable_path = exe_path; | |||
f.output = res.output; |
@@ -1,5 +1,7 @@ | |||
#pragma once | |||
#include <chrono> | |||
#include <optional> | |||
#include <string> | |||
#include <string_view> | |||
#include <vector> | |||
@@ -25,11 +27,25 @@ std::string quote_command(const Container& c) { | |||
struct proc_result { | |||
int signal = 0; | |||
int retc = 0; | |||
bool timed_out = false; | |||
std::string output; | |||
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 |
@@ -6,6 +6,7 @@ | |||
#include <spdlog/spdlog.h> | |||
#include <poll.h> | |||
#include <signal.h> | |||
#include <sys/wait.h> | |||
#include <unistd.h> | |||
@@ -61,8 +62,8 @@ spawn_child(const std::vector<std::string>& command, int stdout_pipe, int close_ | |||
} // 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] = {}; | |||
auto rc = ::pipe(stdio_pipe); | |||
check_rc(rc == 0, "Create stdio pipe for subprocess"); | |||
@@ -70,7 +71,7 @@ proc_result dds::run_proc(const std::vector<std::string>& command) { | |||
int read_pipe = stdio_pipe[0]; | |||
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); | |||
@@ -80,13 +81,23 @@ proc_result dds::run_proc(const std::vector<std::string>& command) { | |||
proc_result res; | |||
using namespace std::chrono_literals; | |||
auto timeout = opts.timeout; | |||
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) { | |||
errno = 0; | |||
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; | |||
buffer.resize(1024); | |||
auto nread = ::read(stdio_fd.fd, buffer.data(), buffer.size()); |
@@ -1,6 +1,7 @@ | |||
#ifdef _WIN32 | |||
#include "./proc.hpp" | |||
#include <neo/assert.hpp> | |||
#include <spdlog/spdlog.h> | |||
#include <wil/resource.h> | |||
@@ -12,6 +13,7 @@ | |||
#include <stdexcept> | |||
using namespace dds; | |||
using namespace std::chrono_literals; | |||
namespace { | |||
@@ -21,8 +23,8 @@ 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.bInheritHandle = TRUE; | |||
@@ -36,6 +38,8 @@ proc_result dds::run_proc(const std::vector<std::string>& cmd) { | |||
} | |||
::SetHandleInformation(reader.get(), HANDLE_FLAG_INHERIT, 0); | |||
::COMMTIMEOUTS timeouts; | |||
::GetCommTimeouts(reader.get(), &timeouts); | |||
wil::unique_process_information proc_info; | |||
@@ -50,7 +54,7 @@ proc_result dds::run_proc(const std::vector<std::string>& cmd) { | |||
nullptr, | |||
nullptr, | |||
true, | |||
0, | |||
CREATE_NEW_PROCESS_GROUP, | |||
nullptr, | |||
nullptr, | |||
&startup_info, | |||
@@ -62,11 +66,30 @@ proc_result dds::run_proc(const std::vector<std::string>& cmd) { | |||
writer.reset(); | |||
std::string output; | |||
proc_result res; | |||
auto timeout = opts.timeout; | |||
while (true) { | |||
const int buffer_size = 256; | |||
char buffer[buffer_size]; | |||
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) { | |||
throw_system_error("Failed while reading from the stdio pipe"); | |||
} | |||
@@ -85,7 +108,6 @@ proc_result dds::run_proc(const std::vector<std::string>& cmd) { | |||
throw_system_error("Failed reading exit code of process"); | |||
} | |||
proc_result res; | |||
res.retc = rc; | |||
res.output = std::move(output); | |||
return res; |
@@ -5,17 +5,17 @@ | |||
"cxx_compiler": "g++-9", | |||
"flags": [ | |||
"-Werror=return-type", | |||
"-fsanitize=address", | |||
// "-fsanitize=address", | |||
], | |||
"cxx_flags": [ | |||
"-fconcepts", | |||
"-std=c++2a", | |||
], | |||
"link_flags": [ | |||
// "-static-libgcc", | |||
// "-static-libstdc++" | |||
"-fsanitize=address", | |||
"-fuse-ld=lld", | |||
"-static-libgcc", | |||
"-static-libstdc++" | |||
// "-fsanitize=address", | |||
// "-fuse-ld=lld", | |||
], | |||
"debug": true, | |||
// "optimize": true, |