mirror of
https://github.com/catchorg/Catch2.git
synced 2024-11-26 15:26:11 +01:00
Use anonymous pipes for stdout/stderr redirection.
Closes #2444. Avoid the overhead of creating and deleting temporary files. 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.
This commit is contained in:
parent
5a1ef7e4a6
commit
ac345f86af
@ -37,6 +37,10 @@
|
|||||||
# define CATCH_CPP17_OR_GREATER
|
# define CATCH_CPP17_OR_GREATER
|
||||||
# endif
|
# endif
|
||||||
|
|
||||||
|
# if (__cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)
|
||||||
|
# define CATCH_CPP20_OR_GREATER
|
||||||
|
# endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Only GCC compiler should be used in this block, so other compilers trying to
|
// Only GCC compiler should be used in this block, so other compilers trying to
|
||||||
|
@ -14,13 +14,16 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
||||||
|
#include <system_error>
|
||||||
#if defined(_MSC_VER)
|
#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
|
||||||
#define dup _dup
|
#define dup _dup
|
||||||
#define dup2 _dup2
|
#define dup2 _dup2
|
||||||
#define fileno _fileno
|
#define fileno _fileno
|
||||||
#else
|
#else
|
||||||
#include <unistd.h> // dup and dup2
|
#include <unistd.h> // close, dup, dup2, fileno, pipe and read
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -60,78 +63,194 @@ namespace Catch {
|
|||||||
|
|
||||||
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
||||||
|
|
||||||
|
inline void close_or_throw(int descriptor)
|
||||||
|
{
|
||||||
|
if (close(descriptor))
|
||||||
|
{
|
||||||
|
throw std::system_error{ errno, std::generic_category() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int dup_or_throw(int descriptor)
|
||||||
|
{
|
||||||
|
int result{ dup(descriptor) };
|
||||||
|
|
||||||
|
if (result == -1)
|
||||||
|
{
|
||||||
|
throw std::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)
|
||||||
|
{
|
||||||
|
throw std::system_error{ errno, std::generic_category() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int fileno_or_throw(std::FILE* file)
|
||||||
|
{
|
||||||
|
int result{ fileno(file) };
|
||||||
|
|
||||||
|
if (result == -1)
|
||||||
|
{
|
||||||
|
throw std::system_error{ errno, std::generic_category() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void pipe_or_throw(int descriptors[2])
|
||||||
|
{
|
||||||
#if defined(_MSC_VER)
|
#if defined(_MSC_VER)
|
||||||
TempFile::TempFile() {
|
constexpr int defaultPipeSize{ 0 };
|
||||||
if (tmpnam_s(m_buffer)) {
|
|
||||||
CATCH_RUNTIME_ERROR("Could not get a temp filename");
|
int result{ _pipe(descriptors, defaultPipeSize, _O_TEXT) };
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
TempFile::TempFile() {
|
int result{ pipe(descriptors) };
|
||||||
m_file = std::tmpfile();
|
|
||||||
if (!m_file) {
|
|
||||||
CATCH_RUNTIME_ERROR("Could not create a temp file.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
TempFile::~TempFile() {
|
if (result)
|
||||||
// TBD: What to do about errors here?
|
{
|
||||||
std::fclose(m_file);
|
throw std::system_error{ errno, std::generic_category() };
|
||||||
// We manually create the file on Windows only, on Linux
|
}
|
||||||
// it will be autodeleted
|
}
|
||||||
|
|
||||||
|
inline size_t read_or_throw(int descriptor, void* buffer, size_t size)
|
||||||
|
{
|
||||||
#if defined(_MSC_VER)
|
#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
|
#endif
|
||||||
|
|
||||||
|
if (result == -1)
|
||||||
|
{
|
||||||
|
throw std::system_error{ errno, std::generic_category() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return static_cast<size_t>(result);
|
||||||
|
}
|
||||||
|
|
||||||
FILE* TempFile::getFile() {
|
inline void fflush_or_throw(std::FILE* file)
|
||||||
return m_file;
|
{
|
||||||
|
if (std::fflush(file))
|
||||||
|
{
|
||||||
|
throw std::system_error{ errno, std::generic_category() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jthread::jthread() noexcept : m_thread{} {}
|
||||||
|
|
||||||
|
template <typename F, typename... Args>
|
||||||
|
jthread::jthread(F&& f, Args&&... args) : m_thread{ std::forward<F>(f), std::forward<Args>(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{} {}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TempFile::getContents() {
|
close_or_throw(m_value); // std::terminate on failure (due to noexcept)
|
||||||
std::stringstream sstr;
|
}
|
||||||
char buffer[100] = {};
|
|
||||||
std::rewind(m_file);
|
UniqueFileDescriptor& UniqueFileDescriptor::operator=(UniqueFileDescriptor&& other) noexcept
|
||||||
while (std::fgets(buffer, sizeof(buffer), m_file)) {
|
{
|
||||||
sstr << buffer;
|
if (this != &other)
|
||||||
}
|
{
|
||||||
return sstr.str();
|
if (m_value != 0)
|
||||||
|
{
|
||||||
|
close_or_throw(m_value); // std::terminate on failure (due to noexcept)
|
||||||
}
|
}
|
||||||
|
|
||||||
OutputRedirect::OutputRedirect(std::string& stdout_dest, std::string& stderr_dest) :
|
m_value = other.m_value;
|
||||||
m_originalStdout(dup(1)),
|
other.m_value = 0;
|
||||||
m_originalStderr(dup(2)),
|
|
||||||
m_stdoutDest(stdout_dest),
|
|
||||||
m_stderrDest(stderr_dest) {
|
|
||||||
dup2(fileno(m_stdoutFile.getFile()), 1);
|
|
||||||
dup2(fileno(m_stderrFile.getFile()), 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OutputRedirect::~OutputRedirect() {
|
return *this;
|
||||||
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);
|
|
||||||
|
|
||||||
dup2(m_originalStdout, 1);
|
constexpr int UniqueFileDescriptor::get() { return m_value; }
|
||||||
dup2(m_originalStderr, 2);
|
|
||||||
|
|
||||||
m_stdoutDest += m_stdoutFile.getContents();
|
inline void create_pipe(UniqueFileDescriptor& readDescriptor, UniqueFileDescriptor& writeDescriptor)
|
||||||
m_stderrDest += m_stderrFile.getContents();
|
{
|
||||||
|
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{ std::move(readDescriptor) }, &result] () mutable {
|
||||||
|
read_thread(std::move(readDescriptor), result); } };
|
||||||
|
|
||||||
|
dup2_or_throw(writeDescriptor.get(), m_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
#endif // CATCH_CONFIG_NEW_CAPTURE
|
||||||
|
|
||||||
@ -139,6 +258,7 @@ namespace Catch {
|
|||||||
|
|
||||||
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
||||||
#if defined(_MSC_VER)
|
#if defined(_MSC_VER)
|
||||||
|
#undef close
|
||||||
#undef dup
|
#undef dup
|
||||||
#undef dup2
|
#undef dup2
|
||||||
#undef fileno
|
#undef fileno
|
||||||
|
@ -16,6 +16,10 @@
|
|||||||
#include <iosfwd>
|
#include <iosfwd>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
||||||
|
#include <thread>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Catch {
|
namespace Catch {
|
||||||
|
|
||||||
class RedirectedStream {
|
class RedirectedStream {
|
||||||
@ -66,50 +70,79 @@ namespace Catch {
|
|||||||
|
|
||||||
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
#if defined(CATCH_CONFIG_NEW_CAPTURE)
|
||||||
|
|
||||||
// Windows's implementation of std::tmpfile is terrible (it tries
|
#if defined(CATCH_CPP20_OR_GREATER)
|
||||||
// to create a file inside system folder, thus requiring elevated
|
using jthread = std::jthread;
|
||||||
// privileges for the binary), so we have to use tmpnam(_s) and
|
#else
|
||||||
// create the file ourselves there.
|
// Just enough of std::jthread from C++20 for the code below.
|
||||||
class TempFile {
|
struct jthread final
|
||||||
public:
|
{
|
||||||
TempFile(TempFile const&) = delete;
|
jthread() noexcept;
|
||||||
TempFile& operator=(TempFile const&) = delete;
|
|
||||||
TempFile(TempFile&&) = delete;
|
|
||||||
TempFile& operator=(TempFile&&) = delete;
|
|
||||||
|
|
||||||
TempFile();
|
template <typename F, typename... Args>
|
||||||
~TempFile();
|
jthread(F&& f, Args&&... args);
|
||||||
|
|
||||||
std::FILE* getFile();
|
jthread(jthread const&) = delete;
|
||||||
std::string getContents();
|
jthread(jthread&&) noexcept = default;
|
||||||
|
|
||||||
private:
|
jthread& operator=(jthread const&) noexcept = delete;
|
||||||
std::FILE* m_file = nullptr;
|
|
||||||
#if defined(_MSC_VER)
|
|
||||||
char m_buffer[L_tmpnam] = { 0 };
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Not exactly like std::jthread, but close enough for the code below.
|
||||||
|
jthread& operator=(jthread&&) noexcept = default;
|
||||||
|
|
||||||
class OutputRedirect {
|
// Not exactly like std::jthread, but close enough for the code below.
|
||||||
public:
|
~jthread() noexcept;
|
||||||
OutputRedirect(OutputRedirect const&) = delete;
|
|
||||||
OutputRedirect& operator=(OutputRedirect const&) = delete;
|
|
||||||
OutputRedirect(OutputRedirect&&) = delete;
|
|
||||||
OutputRedirect& operator=(OutputRedirect&&) = delete;
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::thread m_thread;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
OutputRedirect(std::string& stdout_dest, std::string& stderr_dest);
|
struct UniqueFileDescriptor final
|
||||||
~OutputRedirect();
|
{
|
||||||
|
constexpr UniqueFileDescriptor() noexcept;
|
||||||
|
explicit UniqueFileDescriptor(int value) noexcept;
|
||||||
|
|
||||||
private:
|
UniqueFileDescriptor(UniqueFileDescriptor const&) = delete;
|
||||||
int m_originalStdout = -1;
|
constexpr UniqueFileDescriptor(UniqueFileDescriptor&& other) noexcept;
|
||||||
int m_originalStderr = -1;
|
|
||||||
TempFile m_stdoutFile;
|
~UniqueFileDescriptor() noexcept;
|
||||||
TempFile m_stderrFile;
|
|
||||||
std::string& m_stdoutDest;
|
UniqueFileDescriptor& operator=(UniqueFileDescriptor const&) = delete;
|
||||||
std::string& m_stderrDest;
|
UniqueFileDescriptor& operator=(UniqueFileDescriptor&& other) noexcept;
|
||||||
};
|
|
||||||
|
constexpr int get();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputFileRedirector final
|
||||||
|
{
|
||||||
|
explicit OutputFileRedirector(std::FILE* file, std::string& result);
|
||||||
|
|
||||||
|
OutputFileRedirector(OutputFileRedirector const&) = delete;
|
||||||
|
OutputFileRedirector(OutputFileRedirector&&) = delete;
|
||||||
|
|
||||||
|
~OutputFileRedirector() noexcept;
|
||||||
|
|
||||||
|
OutputFileRedirector& operator=(OutputFileRedirector const&) = delete;
|
||||||
|
OutputFileRedirector& operator=(OutputFileRedirector&&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::FILE* m_file;
|
||||||
|
int m_fd;
|
||||||
|
UniqueFileDescriptor m_previous;
|
||||||
|
jthread m_readThread;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputRedirect final
|
||||||
|
{
|
||||||
|
OutputRedirect(std::string& output, std::string& error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
OutputFileRedirector m_output;
|
||||||
|
OutputFileRedirector m_error;
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user