diff --git a/CMakeLists.txt b/CMakeLists.txt index db830904..c877a3ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,6 +159,7 @@ set(INTERNAL_HEADERS ${HEADER_DIR}/internal/catch_objc.hpp ${HEADER_DIR}/internal/catch_objc_arc.hpp ${HEADER_DIR}/internal/catch_option.hpp + ${HEADER_DIR}/internal/catch_output_redirect.h ${HEADER_DIR}/internal/catch_platform.h ${HEADER_DIR}/internal/catch_random_number_generator.h ${HEADER_DIR}/internal/catch_reenable_warnings.h @@ -225,6 +226,7 @@ set(IMPL_SOURCES ${HEADER_DIR}/internal/catch_matchers_generic.cpp ${HEADER_DIR}/internal/catch_matchers_string.cpp ${HEADER_DIR}/internal/catch_message.cpp + ${HEADER_DIR}/internal/catch_output_redirect.cpp ${HEADER_DIR}/internal/catch_registry_hub.cpp ${HEADER_DIR}/internal/catch_interfaces_reporter.cpp ${HEADER_DIR}/internal/catch_random_number_generator.cpp diff --git a/docs/configuration.md b/docs/configuration.md index dd81dde4..00d012df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -121,6 +121,7 @@ by using `_NO_` in the macro, e.g. `CATCH_CONFIG_NO_CPP17_UNCAUGHT_EXCEPTIONS`. CATCH_CONFIG_DISABLE_STRINGIFICATION // Disable stringifying the original expression CATCH_CONFIG_DISABLE // Disables assertions and test case registration CATCH_CONFIG_WCHAR // Enables use of wchart_t + CATCH_CONFIG_EXPERIMENTAL_REDIRECT // Enables the new (experimental) way of capturing stdout/stderr Currently Catch enables `CATCH_CONFIG_WINDOWS_SEH` only when compiled with MSVC, because some versions of MinGW do not have the necessary Win32 API support. @@ -131,7 +132,9 @@ Currently Catch enables `CATCH_CONFIG_WINDOWS_SEH` only when compiled with MSVC, `CATCH_CONFIG_WCHAR` is on by default, but can be disabled. Currently it is only used in support for DJGPP cross-compiler. -These toggles can be disabled by using `_NO_` form of the toggle, e.g. `CATCH_CONFIG_NO_WINDOWS_SEH`. +With the exception of `CATCH_CONFIG_EXPERIMENTAL_REDIRECT`, +these toggles can be disabled by using `_NO_` form of the toggle, +e.g. `CATCH_CONFIG_NO_WINDOWS_SEH`. ### `CATCH_CONFIG_FAST_COMPILE` Defining this flag speeds up compilation of test files by ~20%, by making 2 changes: diff --git a/include/internal/catch_output_redirect.cpp b/include/internal/catch_output_redirect.cpp new file mode 100644 index 00000000..c3ad826d --- /dev/null +++ b/include/internal/catch_output_redirect.cpp @@ -0,0 +1,131 @@ +/* +* Created by Martin on 28/04/2018. +* +* Distributed under the Boost Software License, Version 1.0. (See accompanying +* file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +*/ + +#include "catch_output_redirect.h" + + + +#include +#include +#include +#include + +#if defined(CATCH_PLATFORM_WINDOWS) +#include //_dup and _dup2 +#define dup _dup +#define dup2 _dup2 +#define fileno _fileno +#else +#include // dup and dup2 +#endif + +namespace Catch { + + RedirectedStream::RedirectedStream( std::ostream& originalStream, std::ostream& redirectionStream ) + : m_originalStream( originalStream ), + m_redirectionStream( redirectionStream ), + m_prevBuf( m_originalStream.rdbuf() ) + { + m_originalStream.rdbuf( m_redirectionStream.rdbuf() ); + } + + RedirectedStream::~RedirectedStream() { + m_originalStream.rdbuf( m_prevBuf ); + } + + RedirectedStdOut::RedirectedStdOut() : m_cout( Catch::cout(), m_rss.get() ) {} + auto RedirectedStdOut::str() const -> std::string { return m_rss.str(); } + + RedirectedStdErr::RedirectedStdErr() + : m_cerr( Catch::cerr(), m_rss.get() ), + m_clog( Catch::clog(), m_rss.get() ) + {} + auto RedirectedStdErr::str() const -> std::string { return m_rss.str(); } + + + +#if defined(CATCH_PLATFORM_WINDOWS) + TempFile::TempFile() { + if (tmpnam_s(m_buffer)) { + throw std::runtime_error("Could not get a temp filename"); + } + if (fopen_s(&m_file, m_buffer, "w")) { + char buffer[100]; + if (strerror_s(buffer, errno)) { + throw std::runtime_error("Could not translate errno to string"); + } + throw std::runtime_error("Could not open the temp file: " + std::string(m_buffer) + buffer); + } + } +#else + TempFile::TempFile() { + m_file = std::tmpfile(); + if (!m_file) { + throw std::runtime_error("Could not create a temp file."); + } + } + +#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 defined(CATCH_PLATFORM_WINDOWS) + std::remove(m_buffer); +#endif + } + + + FILE* TempFile::getFile() { + return m_file; + } + + 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(); + } + + 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); + } + + 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); + + dup2(m_originalStdout, 1); + dup2(m_originalStderr, 2); + + m_stdoutDest += m_stdoutFile.getContents(); + m_stderrDest += m_stderrFile.getContents(); + } + + +} // namespace Catch + +#if defined(CATCH_PLATFORM_WINDOWS) +#undef dup +#undef dup2 +#undef fileno +#endif diff --git a/include/internal/catch_output_redirect.h b/include/internal/catch_output_redirect.h new file mode 100644 index 00000000..121aed64 --- /dev/null +++ b/include/internal/catch_output_redirect.h @@ -0,0 +1,98 @@ +/* + * Created by Martin on 28/04/2018. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef TWOBLUECUBES_CATCH_OUTPUT_REDIRECT_H +#define TWOBLUECUBES_CATCH_OUTPUT_REDIRECT_H + +#include "catch_platform.h" +#include "catch_stream.h" + +#include +#include +#include + +namespace Catch { + + class RedirectedStream { + std::ostream& m_originalStream; + std::ostream& m_redirectionStream; + std::streambuf* m_prevBuf; + + public: + RedirectedStream( std::ostream& originalStream, std::ostream& redirectionStream ); + ~RedirectedStream(); + }; + + class RedirectedStdOut { + ReusableStringStream m_rss; + RedirectedStream m_cout; + public: + RedirectedStdOut(); + auto str() const -> std::string; + }; + + // StdErr has two constituent streams in C++, std::cerr and std::clog + // This means that we need to redirect 2 streams into 1 to keep proper + // order of writes + class RedirectedStdErr { + ReusableStringStream m_rss; + RedirectedStream m_cerr; + RedirectedStream m_clog; + public: + RedirectedStdErr(); + auto str() const -> std::string; + }; + + + // Windows's implementation of std::tmpfile is terrible (it tries + // to create a file inside system folder, thus requiring elevated + // privileges for the binary), so we have to use tmpnam(_s) and + // create the file ourselves there. + class TempFile { + public: + TempFile(TempFile const&) = delete; + TempFile& operator=(TempFile const&) = delete; + TempFile(TempFile&&) = delete; + TempFile& operator=(TempFile&&) = delete; + + TempFile(); + ~TempFile(); + + std::FILE* getFile(); + std::string getContents(); + + private: + std::FILE* m_file = nullptr; + #if defined(CATCH_PLATFORM_WINDOWS) + char m_buffer[L_tmpnam] = { 0 }; + #endif + }; + + + class OutputRedirect { + public: + OutputRedirect(OutputRedirect const&) = delete; + OutputRedirect& operator=(OutputRedirect const&) = delete; + OutputRedirect(OutputRedirect&&) = delete; + OutputRedirect& operator=(OutputRedirect&&) = delete; + + + OutputRedirect(std::string& stdout_dest, std::string& stderr_dest); + ~OutputRedirect(); + + private: + int m_originalStdout = -1; + int m_originalStderr = -1; + TempFile m_stdoutFile; + TempFile m_stderrFile; + std::string& m_stdoutDest; + std::string& m_stderrDest; + }; + + +} // end namespace Catch + +#endif // TWOBLUECUBES_CATCH_OUTPUT_REDIRECT_H diff --git a/include/internal/catch_run_context.cpp b/include/internal/catch_run_context.cpp index 99c77058..97700bef 100644 --- a/include/internal/catch_run_context.cpp +++ b/include/internal/catch_run_context.cpp @@ -3,6 +3,7 @@ #include "catch_enforce.h" #include "catch_random_number_generator.h" #include "catch_stream.h" +#include "catch_output_redirect.h" #include #include @@ -10,48 +11,6 @@ namespace Catch { - class RedirectedStream { - std::ostream& m_originalStream; - std::ostream& m_redirectionStream; - std::streambuf* m_prevBuf; - - public: - RedirectedStream( std::ostream& originalStream, std::ostream& redirectionStream ) - : m_originalStream( originalStream ), - m_redirectionStream( redirectionStream ), - m_prevBuf( m_originalStream.rdbuf() ) - { - m_originalStream.rdbuf( m_redirectionStream.rdbuf() ); - } - ~RedirectedStream() { - m_originalStream.rdbuf( m_prevBuf ); - } - }; - - class RedirectedStdOut { - ReusableStringStream m_rss; - RedirectedStream m_cout; - public: - RedirectedStdOut() : m_cout( Catch::cout(), m_rss.get() ) {} - auto str() const -> std::string { return m_rss.str(); } - }; - - // StdErr has two constituent streams in C++, std::cerr and std::clog - // This means that we need to redirect 2 streams into 1 to keep proper - // order of writes - class RedirectedStdErr { - ReusableStringStream m_rss; - RedirectedStream m_cerr; - RedirectedStream m_clog; - public: - RedirectedStdErr() - : m_cerr( Catch::cerr(), m_rss.get() ), - m_clog( Catch::clog(), m_rss.get() ) - {} - auto str() const -> std::string { return m_rss.str(); } - }; - - RunContext::RunContext(IConfigPtr const& _config, IStreamingReporterPtr&& reporter) : m_runInfo(_config->name()), m_context(getCurrentMutableContext()), @@ -299,13 +258,19 @@ namespace Catch { Timer timer; try { if (m_reporter->getPreferences().shouldRedirectStdOut) { +#if !defined(CATCH_CONFIG_EXPERIMENTAL_REDIRECT) RedirectedStdOut redirectedStdOut; RedirectedStdErr redirectedStdErr; + timer.start(); invokeActiveTestCase(); redirectedCout += redirectedStdOut.str(); redirectedCerr += redirectedStdErr.str(); - +#else + OutputRedirect r(redirectedCout, redirectedCerr); + timer.start(); + invokeActiveTestCase(); +#endif } else { timer.start(); invokeActiveTestCase();