Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use anonymous pipes for stdout/stderr redirection. #2472

Open
wants to merge 5 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/catch2/internal/catch_compiler_capabilities.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
# define CATCH_CPP17_OR_GREATER
# endif

# if (__cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)
# define CATCH_CPP20_OR_GREATER
# endif

#endif

// Only GCC compiler should be used in this block, so other compilers trying to
Expand Down
6 changes: 6 additions & 0 deletions src/catch2/internal/catch_enforce.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <catch2/internal/catch_stdstreams.hpp>

#include <stdexcept>
#include <system_error>


namespace Catch {
Expand Down Expand Up @@ -36,6 +37,11 @@ namespace Catch {
throw_exception(std::runtime_error(msg));
}

[[noreturn]]
void throw_system_error(int ev, const std::error_category& ecat) {
throw_exception(std::system_error(ev, ecat));
}



} // namespace Catch;
5 changes: 5 additions & 0 deletions src/catch2/internal/catch_enforce.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ namespace Catch {
void throw_domain_error(std::string const& msg);
[[noreturn]]
void throw_runtime_error(std::string const& msg);
[[noreturn]]
void throw_system_error(int ev, const std::error_category& ecat);

} // namespace Catch;

Expand All @@ -47,6 +49,9 @@ namespace Catch {
#define CATCH_RUNTIME_ERROR(...) \
Catch::throw_runtime_error(CATCH_MAKE_MSG( __VA_ARGS__ ))

#define CATCH_SYSTEM_ERROR(ev, ecat) \
Catch::throw_system_error((ev), (ecat))

#define CATCH_ENFORCE( condition, ... ) \
do{ if( !(condition) ) CATCH_ERROR( __VA_ARGS__ ); } while(false)

Expand Down
231 changes: 176 additions & 55 deletions src/catch2/internal/catch_output_redirect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
#include <sstream>

#if defined(CATCH_CONFIG_NEW_CAPTURE)
#include <catch2/internal/catch_move_and_forward.hpp>
davidmatson marked this conversation as resolved.
Show resolved Hide resolved
#include <system_error>
#if defined(_MSC_VER)
#include <io.h> //_dup and _dup2
#include <fcntl.h> // _O_TEXT
#include <io.h> // _close, _dup, _dup2, _fileno, _pipe and _read
#define close _close
davidmatson marked this conversation as resolved.
Show resolved Hide resolved
#define dup _dup
#define dup2 _dup2
#define fileno _fileno
#else
#include <unistd.h> // dup and dup2
#include <unistd.h> // close, dup, dup2, fileno, pipe and read
#endif
#endif

Expand Down Expand Up @@ -60,85 +64,202 @@ namespace Catch {

#if defined(CATCH_CONFIG_NEW_CAPTURE)

#if defined(_MSC_VER)
TempFile::TempFile() {
if (tmpnam_s(m_buffer)) {
CATCH_RUNTIME_ERROR("Could not get a temp filename");
}
if (fopen_s(&m_file, m_buffer, "w+")) {
char buffer[100];
if (strerror_s(buffer, errno)) {
CATCH_RUNTIME_ERROR("Could not translate errno to a string");
}
CATCH_RUNTIME_ERROR("Could not open the temp file: '" << m_buffer << "' because: " << buffer);
}
inline void close_or_throw(int descriptor)
davidmatson marked this conversation as resolved.
Show resolved Hide resolved
{
if (close(descriptor))
{
CATCH_SYSTEM_ERROR(errno, std::generic_category());
}
#else
TempFile::TempFile() {
m_file = std::tmpfile();
if (!m_file) {
CATCH_RUNTIME_ERROR("Could not create a temp file.");
}
}

inline int dup_or_throw(int descriptor)
{
int result{ dup(descriptor) };

if (result == -1)
{
CATCH_SYSTEM_ERROR(errno, std::generic_category());
}

return result;
}

inline int dup2_or_throw(int sourceDescriptor, int destinationDescriptor)
{
int result{ dup2(sourceDescriptor, destinationDescriptor) };

if (result == -1)
{
CATCH_SYSTEM_ERROR(errno, std::generic_category());
}

return result;
}

inline int fileno_or_throw(std::FILE* file)
{
int result{ fileno(file) };

if (result == -1)
{
CATCH_SYSTEM_ERROR(errno, std::generic_category());
}

return result;
}

inline void pipe_or_throw(int descriptors[2])
{
#if defined(_MSC_VER)
constexpr int defaultPipeSize{ 0 };

int result{ _pipe(descriptors, defaultPipeSize, _O_TEXT) };
#else
int result{ pipe(descriptors) };
#endif

TempFile::~TempFile() {
// TBD: What to do about errors here?
std::fclose(m_file);
// We manually create the file on Windows only, on Linux
// it will be autodeleted
if (result)
{
CATCH_SYSTEM_ERROR(errno, std::generic_category());
}
}

inline size_t read_or_throw(int descriptor, void* buffer, size_t size)
{
#if defined(_MSC_VER)
std::remove(m_buffer);
int result{ _read(descriptor, buffer, static_cast<unsigned>(size)) };
#else
ssize_t result{ read(descriptor, buffer, size) };
#endif

if (result == -1)
{
CATCH_SYSTEM_ERROR(errno, std::generic_category());
}

return static_cast<size_t>(result);
}

FILE* TempFile::getFile() {
return m_file;
inline void fflush_or_throw(std::FILE* file)
{
if (std::fflush(file))
{
CATCH_SYSTEM_ERROR(errno, std::generic_category());
}
}

std::string TempFile::getContents() {
std::stringstream sstr;
char buffer[100] = {};
std::rewind(m_file);
while (std::fgets(buffer, sizeof(buffer), m_file)) {
sstr << buffer;
}
return sstr.str();
jthread::jthread() noexcept : m_thread{} {}

template <typename F, typename... Args>
jthread::jthread(F&& f, Args&&... args) : m_thread{ CATCH_FORWARD(f), CATCH_FORWARD(args)... } {}

// Not exactly like std::jthread, but close enough for the code below.
jthread::~jthread() noexcept
{
if (m_thread.joinable())
{
m_thread.join();
}
}

constexpr UniqueFileDescriptor::UniqueFileDescriptor() noexcept : m_value{} {}

OutputRedirect::OutputRedirect(std::string& stdout_dest, std::string& stderr_dest) :
m_originalStdout(dup(1)),
m_originalStderr(dup(2)),
m_stdoutDest(stdout_dest),
m_stderrDest(stderr_dest) {
dup2(fileno(m_stdoutFile.getFile()), 1);
dup2(fileno(m_stderrFile.getFile()), 2);
UniqueFileDescriptor::UniqueFileDescriptor(int value) noexcept : m_value{ value } {}

constexpr UniqueFileDescriptor::UniqueFileDescriptor(UniqueFileDescriptor&& other) noexcept :
m_value{ other.m_value }
{
other.m_value = 0;
}

UniqueFileDescriptor::~UniqueFileDescriptor() noexcept
{
if (m_value == 0)
{
return;
}

OutputRedirect::~OutputRedirect() {
Catch::cout() << std::flush;
fflush(stdout);
// Since we support overriding these streams, we flush cerr
// even though std::cerr is unbuffered
Catch::cerr() << std::flush;
Catch::clog() << std::flush;
fflush(stderr);
close_or_throw(m_value); // std::terminate on failure (due to noexcept)
}

dup2(m_originalStdout, 1);
dup2(m_originalStderr, 2);
UniqueFileDescriptor& UniqueFileDescriptor::operator=(UniqueFileDescriptor&& other) noexcept
{
if (this != &other)
{
if (m_value != 0)
{
close_or_throw(m_value); // std::terminate on failure (due to noexcept)
davidmatson marked this conversation as resolved.
Show resolved Hide resolved
}
davidmatson marked this conversation as resolved.
Show resolved Hide resolved

m_stdoutDest += m_stdoutFile.getContents();
m_stderrDest += m_stderrFile.getContents();
m_value = other.m_value;
other.m_value = 0;
}

return *this;
}

constexpr int UniqueFileDescriptor::get() { return m_value; }

inline void create_pipe(UniqueFileDescriptor& readDescriptor, UniqueFileDescriptor& writeDescriptor)
{
readDescriptor = {};
writeDescriptor = {};

int descriptors[2];
pipe_or_throw(descriptors);

readDescriptor = UniqueFileDescriptor{ descriptors[0] };
writeDescriptor = UniqueFileDescriptor{ descriptors[1] };
}

inline void read_thread(UniqueFileDescriptor&& file, std::string& result)
{
std::string buffer{};
constexpr size_t bufferSize{ 4096 };
buffer.resize(bufferSize);
size_t sizeRead{};

while ((sizeRead = read_or_throw(file.get(), &buffer[0], bufferSize)) != 0)
{
result.append(buffer.data(), sizeRead);
}
}

OutputFileRedirector::OutputFileRedirector(FILE* file, std::string& result) :
m_file{ file },
m_fd{ fileno_or_throw(m_file) },
m_previous{ dup_or_throw(m_fd) }
{
fflush_or_throw(m_file);

UniqueFileDescriptor readDescriptor{};
UniqueFileDescriptor writeDescriptor{};
create_pipe(readDescriptor, writeDescriptor);

// Anonymous pipes have a limited buffer and require an active reader to ensure the writer does not become blocked.
// Use a separate thread to ensure the buffer does not get stuck full.
m_readThread = jthread{ [readDescriptor{ CATCH_MOVE(readDescriptor) }, &result] () mutable {
read_thread(CATCH_MOVE(readDescriptor), result); } };

dup2_or_throw(writeDescriptor.get(), m_fd);
davidmatson marked this conversation as resolved.
Show resolved Hide resolved
}

OutputFileRedirector::~OutputFileRedirector() noexcept
{
fflush_or_throw(m_file); // std::terminate on failure (due to noexcept)
dup2_or_throw(m_previous.get(), m_fd); // std::terminate on failure (due to noexcept)
}

OutputRedirect::OutputRedirect(std::string& output, std::string& error) :
m_output{ stdout, output }, m_error{ stderr, error } {}

#endif // CATCH_CONFIG_NEW_CAPTURE

} // namespace Catch

#if defined(CATCH_CONFIG_NEW_CAPTURE)
#if defined(_MSC_VER)
#undef close
#undef dup
#undef dup2
#undef fileno
Expand Down