diff options
| author | Leitwolf11 | 2026-10-23 23:25:41 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-23 23:25:41 +0300 |
| commit | 93d4e7dbebab867e0a7f59fc25308460a7a76280 (patch) | |
| tree | 54c568a3063d49e21f039b6d38f40f51be7991f0 /src/Progmasoft/Runner/Process.cpp | |
| parent | 83c6fc3ac13abe83fa39f7b5f8559ccaf4412233 (diff) | |
| download | catch3-93d4e7dbebab867e0a7f59fc25308460a7a76280.tar.gz | |
Replace Catch2's runner with the Catch3 runner
Catch3 keeps Catch2's API for writing tests (test cases, sections,
assertions, matchers, generators, listeners, benchmarks) and replaces its
session, command line and reporters with a runner of its own.
The runner starts the test binary again as worker processes that take the
next test when they are free. A crash, an exit or a hang ends one test, not
the run. On top of that it adds:
- retries that tell deterministic, flaky and order-dependent failures
apart, and a bisection that names the test leaving state behind;
- a history of verdicts and durations, kept under a file lock, that drives
automatic quarantine, adaptive time limits, predictive ordering,
incremental runs, failure replay and analytics;
- selection by change, past failure or prediction, dependency closure,
and sharding by a stable hash or by recorded durations;
- tags for serial and isolated tests, shared resources, dependencies,
time limits and retries;
- a failure category, a signature, an analysis and a replay command for
every failure;
- console, JUnit XML (with flakyFailure and rerunFailure), JSON and Chrome
trace reports, and a versioned discovery listing (--list=json);
- a temporary directory per test (TMPDIR, TMP, TEMP and
TestTemporaryDirectory());
- Bazel's test environment: XML output file, test filter, sharding, seed,
premature exit file and undeclared outputs.
The command line is parsed by a hand-written, typed parser. Catch::Session
remains as a thin facade over the runner for custom main functions.
The result model in Progmasoft/Catch3/Results.hpp was rewritten around
tests and attempts, and the JUnit writer now writes it. The property
engine's example database is updated under a file lock with an atomic
rename, so parallel workers can write it at the same time.
Removed: Clara, the command-line and reporter-spec parsers, every reporter
and CATCH_REGISTER_REPORTER, listing, text flow, console colour and width,
the JSON and XML writers, sharding in the test registry, approval tests and
their baselines, Conan packaging, and the unused third_party/clara.hpp.
Listeners keep working; reporters' list and skip events are gone.
The Catch2 library is now compiled as C++20; code that includes only
Catch2's headers still needs C++14. Duplicate test cases are still refused
at start-up. Version 0.3.0.
ExtraTests now turn exceptions off for clang-cl with MSVC-style options,
which -fno-exceptions did not do.
Diffstat (limited to 'src/Progmasoft/Runner/Process.cpp')
| -rw-r--r-- | src/Progmasoft/Runner/Process.cpp | 1289 |
1 files changed, 1289 insertions, 0 deletions
diff --git a/src/Progmasoft/Runner/Process.cpp b/src/Progmasoft/Runner/Process.cpp new file mode 100644 index 00000000..8ef02327 --- /dev/null +++ b/src/Progmasoft/Runner/Process.cpp @@ -0,0 +1,1289 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#include <algorithm> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <thread> + +#include "Process.hpp" + +#if defined(_WIN32) +# ifndef NOMINMAX +# define NOMINMAX +# endif +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include <fcntl.h> +# include <io.h> +# include <share.h> +# include <sys/stat.h> +# include <windows.h> +#else +# include <cerrno> +# include <csignal> +# include <fcntl.h> +# include <spawn.h> +# include <sys/types.h> +# include <sys/wait.h> +# include <unistd.h> +# if defined(__APPLE__) +# include <crt_externs.h> +# include <mach-o/dyld.h> +# endif +# if !defined(__APPLE__) && !(defined(__GLIBC__) && defined(_GNU_SOURCE)) +extern char **environ; +# endif +#endif + +namespace Progmasoft::Catch3::Runner +{ + namespace + { +#if defined(_WIN32) + HANDLE + AsHandle(std::intptr_t value) noexcept + { + return reinterpret_cast<HANDLE>(value); + } + + std::intptr_t + FromHandle(HANDLE handle) noexcept + { + return reinterpret_cast<std::intptr_t>(handle); + } + + std::wstring + Widen(std::string_view text) + { + if (text.empty()) + { + return {}; + } + const int size = MultiByteToWideChar(CP_UTF8, + 0, + text.data(), + static_cast<int>(text.size()), + nullptr, + 0); + std::wstring wide(static_cast<std::size_t>(size), L'\0'); + MultiByteToWideChar(CP_UTF8, + 0, + text.data(), + static_cast<int>(text.size()), + wide.data(), + size); + return wide; + } + + std::string + Narrow(std::wstring_view text) + { + if (text.empty()) + { + return {}; + } + const int size = WideCharToMultiByte(CP_UTF8, + 0, + text.data(), + static_cast<int>(text.size()), + nullptr, + 0, + nullptr, + nullptr); + std::string narrow(static_cast<std::size_t>(size), '\0'); + WideCharToMultiByte(CP_UTF8, + 0, + text.data(), + static_cast<int>(text.size()), + narrow.data(), + size, + nullptr, + nullptr); + return narrow; + } + + std::string + LastErrorMessage(const char *what) + { + const DWORD code = GetLastError(); + wchar_t *buffer = nullptr; + const DWORD length = FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + code, + 0, + reinterpret_cast<wchar_t *>(&buffer), + 0, + nullptr); + std::string message + = length > 0 ? Narrow(std::wstring_view{ buffer, length }) + : std::string{}; + if (buffer != nullptr) + { + LocalFree(buffer); + } + while (!message.empty() + && (message.back() == '\n' || message.back() == '\r' + || message.back() == ' ' || message.back() == '.')) + { + message.pop_back(); + } + return std::string{ what } + ": " + message + " (error " + + std::to_string(code) + ")"; + } + + /// Quotes one argument the way the Microsoft C runtime splits a + /// command line back into `argv`. + void + AppendQuotedArgument(std::wstring &commandLine, + const std::wstring &argument) + { + const bool needsQuotes + = argument.empty() + || argument.find_first_of(L" \t\n\v\"") != std::wstring::npos; + if (!needsQuotes) + { + commandLine += argument; + return; + } + commandLine.push_back(L'"'); + std::size_t backslashes = 0; + for (const wchar_t character : argument) + { + if (character == L'\\') + { + ++backslashes; + continue; + } + if (character == L'"') + { + commandLine.append(backslashes * 2 + 1, L'\\'); + } + else + { + commandLine.append(backslashes, L'\\'); + } + backslashes = 0; + commandLine.push_back(character); + } + commandLine.append(backslashes * 2, L'\\'); + commandLine.push_back(L'"'); + } + + std::wstring + EnvironmentBlock( + const std::vector<std::pair<std::string, std::string>> &additions) + { + std::vector<std::wstring> entries; + wchar_t *current = GetEnvironmentStringsW(); + if (current != nullptr) + { + for (const wchar_t *entry = current; *entry != L'\0'; + entry += wcslen(entry) + 1) + { + entries.emplace_back(entry); + } + FreeEnvironmentStringsW(current); + } + for (const auto &[name, value] : additions) + { + const std::wstring wideName = Widen(name); + std::erase_if(entries, [&](const std::wstring &entry) { + // A leading '=' marks the hidden per-drive variables. + const auto equals = entry.find(L'=', 1); + return equals == wideName.size() + && CompareStringOrdinal( + entry.data(), + static_cast<int>(equals), + wideName.data(), + static_cast<int>(wideName.size()), + TRUE) + == CSTR_EQUAL; + }); + entries.push_back(wideName + L"=" + Widen(value)); + } + std::wstring block; + for (const auto &entry : entries) + { + block += entry; + block.push_back(L'\0'); + } + block.push_back(L'\0'); + return block; + } +#else + int + Descriptor(std::intptr_t value) noexcept + { + return static_cast<int>(value); + } + + char ** + CurrentEnvironment() noexcept + { +# if defined(__APPLE__) + return *_NSGetEnviron(); +# else + return environ; +# endif + } + + std::string + ErrnoMessage(const char *what, int error) + { + return std::string{ what } + ": " + std::strerror(error); + } + + bool + MakePipe(int (&descriptors)[2]) + { +# if defined(__linux__) + return pipe2(descriptors, O_CLOEXEC) == 0; +# else + // Only the scheduler thread starts processes, so no other + // thread can fork between creating the pipe and marking it. + if (pipe(descriptors) != 0) + { + return false; + } + fcntl(descriptors[0], F_SETFD, FD_CLOEXEC); + fcntl(descriptors[1], F_SETFD, FD_CLOEXEC); + return true; +# endif + } + + const char * + SignalName(int signal) noexcept + { + switch (signal) + { + case SIGSEGV: + return "SIGSEGV"; + case SIGABRT: + return "SIGABRT"; + case SIGBUS: + return "SIGBUS"; + case SIGFPE: + return "SIGFPE"; + case SIGILL: + return "SIGILL"; + case SIGKILL: + return "SIGKILL"; + case SIGTERM: + return "SIGTERM"; + case SIGPIPE: + return "SIGPIPE"; + case SIGINT: + return "SIGINT"; + default: + return nullptr; + } + } +#endif + +#if defined(_WIN32) + const char * + ExceptionName(std::int64_t code) noexcept + { + switch (static_cast<DWORD>(code)) + { + case 0xC0000005: + return "access violation"; + case 0xC00000FD: + return "stack overflow"; + case 0xC0000094: + return "integer division by zero"; + case 0xC000008E: + return "floating-point division by zero"; + case 0xC000001D: + return "illegal instruction"; + case 0xC0000409: + return "stack buffer overrun or fast fail"; + case 0xC0000374: + return "heap corruption"; + case 0x80000003: + return "breakpoint"; + case 0xE06D7363: + return "unhandled C++ exception"; + default: + return nullptr; + } + } +#endif + } // namespace + + std::string + ExitStatus::Describe() const + { + switch (kind) + { + case Kind::kExited: + return "exit code " + std::to_string(code); + case Kind::kSignaled: + { + std::string text = "signal " + std::to_string(code); +#if !defined(_WIN32) + if (const char *name = SignalName(static_cast<int>(code))) + { + text += std::string{ " (" } + name + ")"; + } +#endif + return text; + } + case Kind::kException: + { + char buffer[32]; + std::snprintf(buffer, + sizeof(buffer), + "0x%08llX", + static_cast<unsigned long long>(code)); + std::string text = std::string{ "exception " } + buffer; +#if defined(_WIN32) + if (const char *name = ExceptionName(code)) + { + text += std::string{ " (" } + name + ")"; + } +#endif + return text; + } + } + return "unknown status"; + } + + PipeReader::PipeReader(PipeReader &&other) noexcept + : handle_{ other.handle_ } + { + other.handle_ = -1; + } + + PipeReader & + PipeReader::operator=(PipeReader &&other) noexcept + { + if (this != &other) + { + Close(); + handle_ = other.handle_; + other.handle_ = -1; + } + return *this; + } + + PipeReader::~PipeReader() + { + Close(); + } + + bool + PipeReader::IsOpen() const noexcept + { + return handle_ != -1; + } + + void + PipeReader::Close() noexcept + { + if (handle_ == -1) + { + return; + } +#if defined(_WIN32) + CloseHandle(AsHandle(handle_)); +#else + close(Descriptor(handle_)); +#endif + handle_ = -1; + } + + std::int64_t + PipeReader::Read(char *buffer, std::size_t size) noexcept + { + if (handle_ == -1) + { + return -1; + } +#if defined(_WIN32) + DWORD read = 0; + const DWORD request + = size > 0x10000000 ? 0x10000000 : static_cast<DWORD>(size); + if (!::ReadFile(AsHandle(handle_), buffer, request, &read, nullptr)) + { + const DWORD error = GetLastError(); + return error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? 0 + : -1; + } + return static_cast<std::int64_t>(read); +#else + while (true) + { + const auto count = read(Descriptor(handle_), buffer, size); + if (count >= 0) + { + return static_cast<std::int64_t>(count); + } + if (errno != EINTR) + { + return -1; + } + } +#endif + } + + PipeWriter::PipeWriter(PipeWriter &&other) noexcept + : handle_{ other.handle_ } + { + other.handle_ = -1; + } + + PipeWriter & + PipeWriter::operator=(PipeWriter &&other) noexcept + { + if (this != &other) + { + Close(); + handle_ = other.handle_; + other.handle_ = -1; + } + return *this; + } + + PipeWriter::~PipeWriter() + { + Close(); + } + + bool + PipeWriter::IsOpen() const noexcept + { + return handle_ != -1; + } + + void + PipeWriter::Close() noexcept + { + if (handle_ == -1) + { + return; + } +#if defined(_WIN32) + CloseHandle(AsHandle(handle_)); +#else + close(Descriptor(handle_)); +#endif + handle_ = -1; + } + + Status + PipeWriter::Write(std::string_view data) noexcept + { + if (handle_ == -1) + { + return Error{ "the pipe is closed" }; + } + while (!data.empty()) + { +#if defined(_WIN32) + DWORD written = 0; + const DWORD request = data.size() > 0x10000000 + ? 0x10000000 + : static_cast<DWORD>(data.size()); + if (!::WriteFile(AsHandle(handle_), + data.data(), + request, + &written, + nullptr)) + { + return Error{ "the other end of the pipe is gone" }; + } + data.remove_prefix(written); +#else + const auto count + = write(Descriptor(handle_), data.data(), data.size()); + if (count < 0) + { + if (errno == EINTR) + { + continue; + } + return Error{ ErrnoMessage("cannot write to the pipe", errno) }; + } + data.remove_prefix(static_cast<std::size_t>(count)); +#endif + } + return Status::Ok(); + } + + ChildProcess::~ChildProcess() + { + if (!exit_) + { + Kill(); + static_cast<void>(Wait()); + } +#if defined(_WIN32) + if (process_ != -1) + { + CloseHandle(AsHandle(process_)); + } + if (group_ != -1) + { + CloseHandle(AsHandle(group_)); + } +#endif + } + +#if defined(_WIN32) + Expected<std::unique_ptr<ChildProcess>> + ChildProcess::Start(const ChildOptions &options) + { + if (options.arguments.empty()) + { + return Error{ "no program to start" }; + } + SECURITY_ATTRIBUTES inheritable{}; + inheritable.nLength = sizeof(inheritable); + inheritable.bInheritHandle = TRUE; + + HANDLE pipes[3][2] = {}; + for (auto &pipe : pipes) + { + if (!CreatePipe(&pipe[0], &pipe[1], &inheritable, 0)) + { + const std::string message + = LastErrorMessage("cannot create a pipe"); + for (auto &created : pipes) + { + for (HANDLE handle : created) + { + if (handle != nullptr) + { + CloseHandle(handle); + } + } + } + return Error{ message }; + } + } + // The child gets the read end of its input and the write ends of its + // output; the ends this process keeps are not inheritable. + HANDLE childInput = pipes[0][0]; + HANDLE parentInput = pipes[0][1]; + HANDLE parentOutput = pipes[1][0]; + HANDLE childOutput = pipes[1][1]; + HANDLE parentErrors = pipes[2][0]; + HANDLE childErrors = pipes[2][1]; + SetHandleInformation(parentInput, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(parentOutput, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(parentErrors, HANDLE_FLAG_INHERIT, 0); + + auto closeChildEnds = [&] { + CloseHandle(childInput); + CloseHandle(childOutput); + CloseHandle(childErrors); + }; + + std::unique_ptr<ChildProcess> child{ new ChildProcess{} }; + child->input_ = PipeWriter{ FromHandle(parentInput) }; + child->output_ = PipeReader{ FromHandle(parentOutput) }; + child->errors_ = PipeReader{ FromHandle(parentErrors) }; + + // Only these three handles are inherited, so a worker never holds + // the pipes of another one open. + HANDLE inherited[3] = { childInput, childOutput, childErrors }; + SIZE_T attributeSize = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeSize); + std::vector<unsigned char> attributeStorage(attributeSize); + auto *attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>( + attributeStorage.data()); + if (!InitializeProcThreadAttributeList(attributes, 1, 0, &attributeSize) + || !UpdateProcThreadAttribute(attributes, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + inherited, + sizeof(inherited), + nullptr, + nullptr)) + { + const std::string message + = LastErrorMessage("cannot prepare the process attributes"); + closeChildEnds(); + return Error{ message }; + } + + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + startup.StartupInfo.hStdInput = childInput; + startup.StartupInfo.hStdOutput = childOutput; + startup.StartupInfo.hStdError = childErrors; + startup.lpAttributeList = attributes; + + std::wstring program = Widen(options.arguments.front()); + std::wstring commandLine; + for (std::size_t index = 0; index < options.arguments.size(); ++index) + { + if (index > 0) + { + commandLine.push_back(L' '); + } + AppendQuotedArgument(commandLine, Widen(options.arguments[index])); + } + std::wstring environment = EnvironmentBlock(options.environment); + const std::wstring directory = options.workingDirectory.wstring(); + + PROCESS_INFORMATION information{}; + const BOOL created + = CreateProcessW(options.searchPath ? nullptr : program.c_str(), + commandLine.data(), + nullptr, + nullptr, + TRUE, + CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED + | EXTENDED_STARTUPINFO_PRESENT, + environment.data(), + directory.empty() ? nullptr : directory.c_str(), + &startup.StartupInfo, + &information); + DeleteProcThreadAttributeList(attributes); + if (!created) + { + const std::string message = LastErrorMessage( + ("cannot start '" + options.arguments.front() + "'").c_str()); + closeChildEnds(); + return Error{ message }; + } + closeChildEnds(); + + // A job ends the whole tree at once, and its limit keeps a crash from + // waiting on the error-reporting dialog. + HANDLE job = CreateJobObjectW(nullptr, nullptr); + if (job != nullptr) + { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; + limits.BasicLimitInformation.LimitFlags + = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION; + SetInformationJobObject(job, + JobObjectExtendedLimitInformation, + &limits, + sizeof(limits)); + if (!AssignProcessToJobObject(job, information.hProcess)) + { + CloseHandle(job); + job = nullptr; + } + } + ResumeThread(information.hThread); + CloseHandle(information.hThread); + + child->id_ = static_cast<std::int64_t>(information.dwProcessId); + child->process_ = FromHandle(information.hProcess); + child->group_ = job == nullptr ? -1 : FromHandle(job); + return child; + } + + void + ChildProcess::Kill() noexcept + { + if (exit_) + { + return; + } + if (group_ != -1) + { + TerminateJobObject(AsHandle(group_), 0xC000013A); + } + else if (process_ != -1) + { + TerminateProcess(AsHandle(process_), 0xC000013A); + } + } + + ExitStatus + ChildProcess::Wait() noexcept + { + if (exit_) + { + return *exit_; + } + ExitStatus status; + if (process_ != -1) + { + WaitForSingleObject(AsHandle(process_), INFINITE); + DWORD code = 0; + GetExitCodeProcess(AsHandle(process_), &code); + status.code = static_cast<std::int64_t>(code); + // NTSTATUS error codes have both top bits set. + status.kind + = (code & 0xC0000000U) == 0xC0000000U || code == 0x80000003U + ? ExitStatus::Kind::kException + : ExitStatus::Kind::kExited; + if (status.kind == ExitStatus::Kind::kExited && code > 0x7FFFFFFFU) + { + status.code = static_cast<std::int32_t>(code); + } + } + exit_ = status; + return status; + } + + std::optional<ExitStatus> + ChildProcess::WaitFor(std::chrono::milliseconds timeout) noexcept + { + if (exit_ || process_ == -1) + { + return Wait(); + } + const auto waited = WaitForSingleObject( + AsHandle(process_), + static_cast<DWORD>(std::max<long long>(0, timeout.count()))); + if (waited == WAIT_TIMEOUT) + { + return std::nullopt; + } + return Wait(); + } + + Expected<WorkerStreams> + TakeStandardStreams() + { + HANDLE process = GetCurrentProcess(); + HANDLE input = nullptr; + HANDLE output = nullptr; + if (!DuplicateHandle(process, + GetStdHandle(STD_INPUT_HANDLE), + process, + &input, + 0, + FALSE, + DUPLICATE_SAME_ACCESS) + || !DuplicateHandle(process, + GetStdHandle(STD_OUTPUT_HANDLE), + process, + &output, + 0, + FALSE, + DUPLICATE_SAME_ACCESS)) + { + return Error{ LastErrorMessage( + "cannot take the standard streams") }; + } + std::fflush(stdout); + int nullDescriptor = -1; + _sopen_s(&nullDescriptor, + "NUL", + _O_RDONLY | _O_BINARY, + _SH_DENYNO, + _S_IREAD); + if (nullDescriptor >= 0) + { + _dup2(nullDescriptor, 0); + _close(nullDescriptor); + } + _dup2(2, 1); + // The runner writes what the worker prints through its own text-mode + // streams; translating line ends here as well would double them. + _setmode(1, _O_BINARY); + _setmode(2, _O_BINARY); + SetStdHandle(STD_INPUT_HANDLE, + reinterpret_cast<HANDLE>(_get_osfhandle(0))); + SetStdHandle(STD_OUTPUT_HANDLE, GetStdHandle(STD_ERROR_HANDLE)); + WorkerStreams streams; + streams.commands = PipeReader{ FromHandle(input) }; + streams.events = PipeWriter{ FromHandle(output) }; + return streams; + } + + std::filesystem::path + CurrentExecutable(const char *argumentZero) + { + std::wstring buffer(MAX_PATH, L'\0'); + while (true) + { + const DWORD length + = GetModuleFileNameW(nullptr, + buffer.data(), + static_cast<DWORD>(buffer.size())); + if (length == 0) + { + return argumentZero == nullptr + ? std::filesystem::path{} + : std::filesystem::path{ argumentZero }; + } + if (length < buffer.size()) + { + buffer.resize(length); + return std::filesystem::path{ buffer }; + } + buffer.resize(buffer.size() * 2); + } + } + + std::string + ReadEnvironment(const char *name) + { + const std::wstring wideName = Widen(name); + const DWORD size + = GetEnvironmentVariableW(wideName.c_str(), nullptr, 0); + if (size == 0) + { + return {}; + } + std::wstring value(size, L'\0'); + const DWORD length + = GetEnvironmentVariableW(wideName.c_str(), value.data(), size); + value.resize(length); + return Narrow(value); + } + + bool + HasEnvironment(const char *name) + { + const std::wstring wideName = Widen(name); + return GetEnvironmentVariableW(wideName.c_str(), nullptr, 0) > 0 + || GetLastError() != ERROR_ENVVAR_NOT_FOUND; + } + + bool + IsStandardOutputTerminal() noexcept + { + DWORD mode = 0; + return GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &mode) != 0; + } + + bool + EnableTerminalColours() noexcept + { + HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD mode = 0; + if (!GetConsoleMode(output, &mode)) + { + return false; + } + return SetConsoleMode(output, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + != 0; + } + + void + QuietCrashReporting() noexcept + { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX + | SEM_NOOPENFILEERRORBOX); + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + } + + std::int64_t + CurrentProcessId() noexcept + { + return static_cast<std::int64_t>(GetCurrentProcessId()); + } + + void + WriteStandardError(std::string_view text) noexcept + { + while (!text.empty()) + { + const unsigned chunk = static_cast<unsigned>( + std::min<std::size_t>(text.size(), 1U << 20U)); + const int written = _write(2, text.data(), chunk); + if (written <= 0) + { + return; + } + text.remove_prefix(static_cast<std::size_t>(written)); + } + } +#else + Expected<std::unique_ptr<ChildProcess>> + ChildProcess::Start(const ChildOptions &options) + { + if (options.arguments.empty()) + { + return Error{ "no program to start" }; + } + int input[2] = { -1, -1 }; + int output[2] = { -1, -1 }; + int errors[2] = { -1, -1 }; + auto closeAll = [&] { + for (int *pipe : { input, output, errors }) + { + for (int index = 0; index < 2; ++index) + { + if (pipe[index] != -1) + { + close(pipe[index]); + pipe[index] = -1; + } + } + } + }; + if (!MakePipe(input) || !MakePipe(output) || !MakePipe(errors)) + { + const int error = errno; + closeAll(); + return Error{ ErrnoMessage("cannot create a pipe", error) }; + } + + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_adddup2(&actions, input[0], 0); + posix_spawn_file_actions_adddup2(&actions, output[1], 1); + posix_spawn_file_actions_adddup2(&actions, errors[1], 2); + + posix_spawnattr_t attributes; + posix_spawnattr_init(&attributes); + sigset_t defaults; + sigemptyset(&defaults); + // The runner ignores SIGPIPE; a test must see the default. + sigaddset(&defaults, SIGPIPE); + posix_spawnattr_setsigdefault(&attributes, &defaults); + sigset_t mask; + sigemptyset(&mask); + posix_spawnattr_setsigmask(&att |