Skip to content

Commit

Permalink
WIP: new redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
horenmar committed Aug 10, 2024
1 parent 8898cc6 commit 4c146cb
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 85 deletions.
262 changes: 238 additions & 24 deletions src/catch2/internal/catch_output_redirect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,37 +27,214 @@

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() );
}
namespace {
//! A no-op implementation, used if no reporter wants output redirection.
class NoopRedirect : public OutputRedirectNew {
void activateImpl() override {}
void deactivateImpl() override {}
std::string getStdout() override { return {}; }
std::string getStderr() override { return {}; }
void clearBuffers() override {}
};

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(); }
/**
* Redirects specific stream's rdbuf with another's.
*
* Redirection can be stopped and started on-demand, assumes
* that the underlying stream's rdbuf aren't changed by other
* users.
*/
class RedirectedStreamNew {
std::ostream& m_originalStream;
std::ostream& m_redirectionStream;
std::streambuf* m_prevBuf;

public:
RedirectedStreamNew( std::ostream& originalStream,
std::ostream& redirectionStream ):
m_originalStream( originalStream ),
m_redirectionStream( redirectionStream ),
m_prevBuf( m_originalStream.rdbuf() ) {}

void startRedirect() {
m_originalStream.rdbuf( m_redirectionStream.rdbuf() );
}
void stopRedirect() { m_originalStream.rdbuf( m_prevBuf ); }
};

/**
* Redirects the `std::cout`, `std::cerr`, `std::clog` streams,
* but does not touch the actual `stdout`/`stderr` file descriptors.
*/
class StreamRedirect : public OutputRedirectNew {
ReusableStringStream m_redirectedOut, m_redirectedErr;
RedirectedStreamNew m_cout, m_cerr, m_clog;
StreamRedirect():
m_cout( Catch::cout(), m_redirectedOut.get() ),
m_cerr( Catch::cerr(), m_redirectedErr.get() ),
m_clog( Catch::clog(), m_redirectedErr.get() ) {}

void activateImpl() override {
m_cout.startRedirect();
m_cerr.startRedirect();
m_clog.startRedirect();
}
void deactivateImpl() override {
m_cout.stopRedirect();
m_cerr.stopRedirect();
m_clog.stopRedirect();
}
std::string getStdout() override { return m_redirectedOut.str(); }
std::string getStderr() override { return m_redirectedErr.str(); }
void clearBuffers() override {
m_redirectedOut.str( "" );
m_redirectedErr.str( "" );
}
};

// 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 TempFile2 {
public:
TempFile2( TempFile2 const& ) = delete;
TempFile2& operator=( TempFile2 const& ) = delete;
TempFile2( TempFile2&& ) = delete;
TempFile2& operator=( TempFile2&& ) = delete;

#if defined( _MSC_VER )
TempFile2() {
if ( tmpnam_s( m_buffer ) ) {
CATCH_RUNTIME_ERROR( "Could not get a temp filename" );
}
if ( fopen_s( &m_file, m_buffer, "wb+" ) ) {
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
TempFile2() {
m_file = std::tmpfile();
if ( !m_file ) {
CATCH_RUNTIME_ERROR( "Could not create a temp file." );
}
}
#endif

~TempFile2() {
// 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( _MSC_VER )
std::remove( m_buffer );
#endif
}

std::FILE* getFile() { return m_file; }
std::string getContents() {
ReusableStringStream sstr;
char buffer[100] = {};
std::rewind( m_file );
while ( std::fgets( buffer, sizeof( buffer ), m_file ) ) {
sstr << buffer;
}
return sstr.str();
}

void clear() {
std::rewind( m_file );
}

private:
std::FILE* m_file = nullptr;
#if defined( _MSC_VER )
char m_buffer[L_tmpnam] = { 0 };
#endif
};

/**
* Redirects the actual `stdout`/`stderr` file descriptors.
*
* Works by replacing the file descriptors numbered 1 and 2
* with an open temporary file.
*/
class FileRedirect : public OutputRedirectNew {
TempFile2 m_outFile, m_errFile;
int m_originalOut = -1;
int m_originalErr = -1;

// Flushes cout/cerr/clog streams and stdout/stderr FDs
void flushEverything() {
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 );
}

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(); }
public:
FileRedirect():
m_originalOut( dup( fileno( stdout ) ) ),
m_originalErr( dup( fileno( stderr ) ) ) {}

RedirectedStreams::RedirectedStreams(std::string& redirectedCout, std::string& redirectedCerr)
: m_redirectedCout(redirectedCout),
m_redirectedCerr(redirectedCerr)
{}
std::string getStdout() override { return m_outFile.getContents(); }
std::string getStderr() override { return m_errFile.getContents(); }
void clearBuffers() override {
m_outFile.clear();
m_errFile.clear();
}

void activateImpl() override {
// We flush before starting redirect, to ensure that we do
// not capture the end of message sent before activation.
flushEverything();

// TODO: error handling.
dup2( fileno( m_outFile.getFile() ), fileno( stdout ) );
dup2( fileno( m_errFile.getFile() ), fileno( stderr ) );
}
void deactivateImpl() override {
// We flush before ending redirect, to ensure that we
// capture all messages sent while the redirect was active.
flushEverything();

// TODO: error handling.
dup2( m_originalOut, fileno( stdout ) );
dup2( m_originalErr, fileno( stderr ) );
}
};

} // end namespace

Detail::unique_ptr<OutputRedirectNew> makeOutputRedirect( bool actual ) {
if ( actual ) {
return Detail::make_unique<FileRedirect>();
} else {
return Detail::make_unique<NoopRedirect>();
}
}

RedirectedStreams::~RedirectedStreams() {
m_redirectedCout += m_redirectedStdOut.str();
m_redirectedCerr += m_redirectedStdErr.str();
RedirectGuard scopedActivate( OutputRedirectNew& redirectImpl ) {
return RedirectGuard( true, redirectImpl );
}

RedirectGuard scopedDeactivate( OutputRedirectNew& redirectImpl ) {
return RedirectGuard( false, redirectImpl );
}

OutputRedirectNew::~OutputRedirectNew() = default;


#if defined(CATCH_CONFIG_NEW_CAPTURE)

#if defined(_MSC_VER)
Expand Down Expand Up @@ -135,6 +312,43 @@ namespace Catch {

#endif // CATCH_CONFIG_NEW_CAPTURE


// TODO: pass pointer?
RedirectGuard::RedirectGuard( bool activate,
OutputRedirectNew& redirectImpl ):
m_redirect( &redirectImpl ) , m_activate( activate ) {
if ( m_activate ) {
m_redirect->activate();
} else {
m_redirect->deactivate();
}
}

RedirectGuard::~RedirectGuard() noexcept( false ) {
if ( m_moved ) { return; }

if ( m_activate ) {
m_redirect->deactivate();
} else {
m_redirect->activate();
}
}

RedirectGuard::RedirectGuard( RedirectGuard&& rhs ) noexcept:
m_redirect( rhs.m_redirect ),
m_activate( rhs.m_activate ),
m_moved( false ) {
rhs.m_moved = true;
}

RedirectGuard& RedirectGuard::operator=( RedirectGuard&& rhs) noexcept {
m_redirect = rhs.m_redirect;
m_activate = rhs.m_activate;
m_moved = false;
rhs.m_moved = true;
return *this;
}

} // namespace Catch

#if defined(CATCH_CONFIG_NEW_CAPTURE)
Expand Down
75 changes: 38 additions & 37 deletions src/catch2/internal/catch_output_redirect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,59 +11,60 @@
#include <catch2/internal/catch_platform.hpp>
#include <catch2/internal/catch_reusable_string_stream.hpp>
#include <catch2/internal/catch_compiler_capabilities.hpp>
#include <catch2/internal/catch_unique_ptr.hpp>

#include <cassert>
#include <cstdio>
#include <iosfwd>
#include <string>

namespace Catch {

class RedirectedStream {
std::ostream& m_originalStream;
std::ostream& m_redirectionStream;
std::streambuf* m_prevBuf;

class OutputRedirectNew {
bool m_redirectActive = false;
virtual void activateImpl() = 0;
virtual void deactivateImpl() = 0;
public:
RedirectedStream( std::ostream& originalStream, std::ostream& redirectionStream );
~RedirectedStream();
virtual ~OutputRedirectNew(); // = default;

// TODO: check that redirect is not active before retrieving stdout/stderr?
virtual std::string getStdout() = 0;
virtual std::string getStderr() = 0;
virtual void clearBuffers() = 0;
void activate() {
assert( !m_redirectActive && "redirect is already active" );
activateImpl();
m_redirectActive = true;
}
void deactivate() {
assert( m_redirectActive && "redirect is not active" );
deactivateImpl();
m_redirectActive = false;
}
};

class RedirectedStdOut {
ReusableStringStream m_rss;
RedirectedStream m_cout;
public:
RedirectedStdOut();
auto str() const -> std::string;
};
Detail::unique_ptr<OutputRedirectNew> makeOutputRedirect( bool actual );

// 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;
};
class RedirectGuard {
OutputRedirectNew* m_redirect;
bool m_activate;
bool m_moved = false;

class RedirectedStreams {
public:
RedirectedStreams(RedirectedStreams const&) = delete;
RedirectedStreams& operator=(RedirectedStreams const&) = delete;
RedirectedStreams(RedirectedStreams&&) = delete;
RedirectedStreams& operator=(RedirectedStreams&&) = delete;
RedirectGuard( bool activate, OutputRedirectNew& redirectImpl );
~RedirectGuard() noexcept( false );

RedirectedStreams(std::string& redirectedCout, std::string& redirectedCerr);
~RedirectedStreams();
private:
std::string& m_redirectedCout;
std::string& m_redirectedCerr;
RedirectedStdOut m_redirectedStdOut;
RedirectedStdErr m_redirectedStdErr;
RedirectGuard( RedirectGuard const& ) = delete;
RedirectGuard& operator=( RedirectGuard const& ) = delete;

// C++14 needs move-able guards to return them from functions
RedirectGuard( RedirectGuard&& rhs ) noexcept;
RedirectGuard& operator=( RedirectGuard&& rhs ) noexcept;
};

RedirectGuard scopedActivate( OutputRedirectNew& redirectImpl );
RedirectGuard scopedDeactivate( OutputRedirectNew& redirectImpl );

#if defined(CATCH_CONFIG_NEW_CAPTURE)

// Windows's implementation of std::tmpfile is terrible (it tries
Expand Down
Loading

0 comments on commit 4c146cb

Please sign in to comment.