| @@ -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, | |||