| /* |
| * Created by Phil Nash on 19th December 2014 |
| * Copyright 2014 Two Blue Cubes Ltd. All rights reserved. |
| * |
| * 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_REPORTER_TEAMCITY_HPP_INCLUDED |
| #define TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED |
| |
| // Don't #include any Catch headers here - we can assume they are already |
| // included before this header. |
| // This is not good practice in general but is necessary in this case so this |
| // file can be distributed as a single header that works with the main |
| // Catch single header. |
| |
| #include <cstring> |
| |
| #ifdef __clang__ |
| # pragma clang diagnostic push |
| # pragma clang diagnostic ignored "-Wpadded" |
| #endif |
| |
| namespace Catch { |
| |
| struct TeamCityReporter : StreamingReporterBase<TeamCityReporter> { |
| TeamCityReporter( ReporterConfig const& _config ) |
| : StreamingReporterBase( _config ) |
| { |
| m_reporterPrefs.shouldRedirectStdOut = true; |
| } |
| |
| static std::string escape( std::string const& str ) { |
| std::string escaped = str; |
| replaceInPlace( escaped, "|", "||" ); |
| replaceInPlace( escaped, "'", "|'" ); |
| replaceInPlace( escaped, "\n", "|n" ); |
| replaceInPlace( escaped, "\r", "|r" ); |
| replaceInPlace( escaped, "[", "|[" ); |
| replaceInPlace( escaped, "]", "|]" ); |
| return escaped; |
| } |
| ~TeamCityReporter() override; |
| |
| static std::string getDescription() { |
| return "Reports test results as TeamCity service messages"; |
| } |
| |
| void skipTest( TestCaseInfo const& /* testInfo */ ) override { |
| } |
| |
| void noMatchingTestCases( std::string const& /* spec */ ) override {} |
| |
| void testGroupStarting( GroupInfo const& groupInfo ) override { |
| StreamingReporterBase::testGroupStarting( groupInfo ); |
| stream << "##teamcity[testSuiteStarted name='" |
| << escape( groupInfo.name ) << "']\n"; |
| } |
| void testGroupEnded( TestGroupStats const& testGroupStats ) override { |
| StreamingReporterBase::testGroupEnded( testGroupStats ); |
| stream << "##teamcity[testSuiteFinished name='" |
| << escape( testGroupStats.groupInfo.name ) << "']\n"; |
| } |
| |
| |
| void assertionStarting( AssertionInfo const& ) override {} |
| |
| bool assertionEnded( AssertionStats const& assertionStats ) override { |
| AssertionResult const& result = assertionStats.assertionResult; |
| if( !result.isOk() ) { |
| |
| ReusableStringStream msg; |
| if( !m_headerPrintedForThisSection ) |
| printSectionHeader( msg.get() ); |
| m_headerPrintedForThisSection = true; |
| |
| msg << result.getSourceInfo() << "\n"; |
| |
| switch( result.getResultType() ) { |
| case ResultWas::ExpressionFailed: |
| msg << "expression failed"; |
| break; |
| case ResultWas::ThrewException: |
| msg << "unexpected exception"; |
| break; |
| case ResultWas::FatalErrorCondition: |
| msg << "fatal error condition"; |
| break; |
| case ResultWas::DidntThrowException: |
| msg << "no exception was thrown where one was expected"; |
| break; |
| case ResultWas::ExplicitFailure: |
| msg << "explicit failure"; |
| break; |
| |
| // We shouldn't get here because of the isOk() test |
| case ResultWas::Ok: |
| case ResultWas::Info: |
| case ResultWas::Warning: |
| CATCH_ERROR( "Internal error in TeamCity reporter" ); |
| // These cases are here to prevent compiler warnings |
| case ResultWas::Unknown: |
| case ResultWas::FailureBit: |
| case ResultWas::Exception: |
| CATCH_ERROR( "Not implemented" ); |
| } |
| if( assertionStats.infoMessages.size() == 1 ) |
| msg << " with message:"; |
| if( assertionStats.infoMessages.size() > 1 ) |
| msg << " with messages:"; |
| for( auto const& messageInfo : assertionStats.infoMessages ) |
| msg << "\n \"" << messageInfo.message << "\""; |
| |
| |
| if( result.hasExpression() ) { |
| msg << |
| "\n " << result.getExpressionInMacro() << "\n" |
| "with expansion:\n" << |
| " " << result.getExpandedExpression() << "\n"; |
| } |
| |
| if( currentTestCaseInfo->okToFail() ) { |
| msg << "- failure ignore as test marked as 'ok to fail'\n"; |
| stream << "##teamcity[testIgnored" |
| << " name='" << escape( currentTestCaseInfo->name )<< "'" |
| << " message='" << escape( msg.str() ) << "'" |
| << "]\n"; |
| } |
| else { |
| stream << "##teamcity[testFailed" |
| << " name='" << escape( currentTestCaseInfo->name )<< "'" |
| << " message='" << escape( msg.str() ) << "'" |
| << "]\n"; |
| } |
| } |
| stream.flush(); |
| return true; |
| } |
| |
| void sectionStarting( SectionInfo const& sectionInfo ) override { |
| m_headerPrintedForThisSection = false; |
| StreamingReporterBase::sectionStarting( sectionInfo ); |
| } |
| |
| void testCaseStarting( TestCaseInfo const& testInfo ) override { |
| m_testTimer.start(); |
| StreamingReporterBase::testCaseStarting( testInfo ); |
| stream << "##teamcity[testStarted name='" |
| << escape( testInfo.name ) << "']\n"; |
| stream.flush(); |
| } |
| |
| void testCaseEnded( TestCaseStats const& testCaseStats ) override { |
| StreamingReporterBase::testCaseEnded( testCaseStats ); |
| if( !testCaseStats.stdOut.empty() ) |
| stream << "##teamcity[testStdOut name='" |
| << escape( testCaseStats.testInfo.name ) |
| << "' out='" << escape( testCaseStats.stdOut ) << "']\n"; |
| if( !testCaseStats.stdErr.empty() ) |
| stream << "##teamcity[testStdErr name='" |
| << escape( testCaseStats.testInfo.name ) |
| << "' out='" << escape( testCaseStats.stdErr ) << "']\n"; |
| stream << "##teamcity[testFinished name='" |
| << escape( testCaseStats.testInfo.name ) << "' duration='" |
| << m_testTimer.getElapsedMilliseconds() << "']\n"; |
| stream.flush(); |
| } |
| |
| private: |
| void printSectionHeader( std::ostream& os ) { |
| assert( !m_sectionStack.empty() ); |
| |
| if( m_sectionStack.size() > 1 ) { |
| os << getLineOfChars<'-'>() << "\n"; |
| |
| std::vector<SectionInfo>::const_iterator |
| it = m_sectionStack.begin()+1, // Skip first section (test case) |
| itEnd = m_sectionStack.end(); |
| for( ; it != itEnd; ++it ) |
| printHeaderString( os, it->name ); |
| os << getLineOfChars<'-'>() << "\n"; |
| } |
| |
| SourceLineInfo lineInfo = m_sectionStack.front().lineInfo; |
| |
| os << lineInfo << "\n"; |
| os << getLineOfChars<'.'>() << "\n\n"; |
| } |
| |
| // if string has a : in first line will set indent to follow it on |
| // subsequent lines |
| static void printHeaderString( std::ostream& os, std::string const& _string, std::size_t indent = 0 ) { |
| std::size_t i = _string.find( ": " ); |
| if( i != std::string::npos ) |
| i+=2; |
| else |
| i = 0; |
| os << Column( _string ) |
| .indent( indent+i) |
| .initialIndent( indent ) << "\n"; |
| } |
| private: |
| bool m_headerPrintedForThisSection = false; |
| Timer m_testTimer; |
| }; |
| |
| #ifdef CATCH_IMPL |
| TeamCityReporter::~TeamCityReporter() {} |
| #endif |
| |
| CATCH_REGISTER_REPORTER( "teamcity", TeamCityReporter ) |
| |
| } // end namespace Catch |
| |
| #ifdef __clang__ |
| # pragma clang diagnostic pop |
| #endif |
| |
| #endif // TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED |