| }; | }; | ||||
| 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, |