/* * Created by Phil on 26/11/2010. * Copyright 2010 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) */ #include "catch_reporter_bases.hpp" #include "catch_reporter_junit.h" #include "../internal/catch_tostring.h" #include "../internal/catch_reporter_registrars.hpp" #include "../internal/catch_text.h" #include #include #include #include namespace Catch { namespace { std::string getCurrentTimestamp() { // Beware, this is not reentrant because of backward compatibility issues // Also, UTC only, again because of backward compatibility (%z is C++11) time_t rawtime; std::time(&rawtime); auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); #ifdef _MSC_VER std::tm timeInfo = {}; gmtime_s(&timeInfo, &rawtime); #else std::tm* timeInfo; timeInfo = std::gmtime(&rawtime); #endif char timeStamp[timeStampSize]; const char * const fmt = "%Y-%m-%dT%H:%M:%SZ"; #ifdef _MSC_VER std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); #else std::strftime(timeStamp, timeStampSize, fmt, timeInfo); #endif return std::string(timeStamp); } std::string fileNameTag(const std::vector &tags) { auto it = std::find_if(begin(tags), end(tags), [] (std::string const& tag) {return tag.front() == '#'; }); if (it != tags.end()) return it->substr(1); return std::string(); } } // anonymous namespace JunitReporter::JunitReporter( ReporterConfig const& _config ) : CumulativeReporterBase( _config ), xml( _config.stream() ) { m_reporterPrefs.shouldRedirectStdOut = true; m_reporterPrefs.shouldReportAllAssertions = true; } JunitReporter::~JunitReporter() {} std::string JunitReporter::getDescription() { return "Reports test results in an XML format that looks like Ant's junitreport target"; } void JunitReporter::noMatchingTestCases( std::string const& /*spec*/ ) {} void JunitReporter::testRunStarting( TestRunInfo const& runInfo ) { CumulativeReporterBase::testRunStarting( runInfo ); xml.startElement( "testsuites" ); } void JunitReporter::testGroupStarting( GroupInfo const& groupInfo ) { suiteTimer.start(); stdOutForSuite.clear(); stdErrForSuite.clear(); unexpectedExceptions = 0; CumulativeReporterBase::testGroupStarting( groupInfo ); } void JunitReporter::testCaseStarting( TestCaseInfo const& testCaseInfo ) { m_okToFail = testCaseInfo.okToFail(); } bool JunitReporter::assertionEnded( AssertionStats const& assertionStats ) { if( assertionStats.assertionResult.getResultType() == ResultWas::ThrewException && !m_okToFail ) unexpectedExceptions++; return CumulativeReporterBase::assertionEnded( assertionStats ); } void JunitReporter::testCaseEnded( TestCaseStats const& testCaseStats ) { stdOutForSuite += testCaseStats.stdOut; stdErrForSuite += testCaseStats.stdErr; CumulativeReporterBase::testCaseEnded( testCaseStats ); } void JunitReporter::testGroupEnded( TestGroupStats const& testGroupStats ) { double suiteTime = suiteTimer.getElapsedSeconds(); CumulativeReporterBase::testGroupEnded( testGroupStats ); writeGroup( *m_testGroups.back(), suiteTime ); } void JunitReporter::testRunEndedCumulative() { xml.endElement(); } void JunitReporter::writeGroup( TestGroupNode const& groupNode, double suiteTime ) { XmlWriter::ScopedElement e = xml.scopedElement( "testsuite" ); TestGroupStats const& stats = groupNode.value; xml.writeAttribute( "name", stats.groupInfo.name ); xml.writeAttribute( "errors", unexpectedExceptions ); xml.writeAttribute( "failures", stats.totals.assertions.failed-unexpectedExceptions ); xml.writeAttribute( "tests", stats.totals.assertions.total() ); xml.writeAttribute( "hostname", "tbd" ); // !TBD if( m_config->showDurations() == ShowDurations::Never ) xml.writeAttribute( "time", "" ); else xml.writeAttribute( "time", suiteTime ); xml.writeAttribute( "timestamp", getCurrentTimestamp() ); // Write properties if there are any if (m_config->hasTestFilters() || m_config->rngSeed() != 0) { auto properties = xml.scopedElement("properties"); if (m_config->hasTestFilters()) { xml.scopedElement("property") .writeAttribute("name", "filters") .writeAttribute("value", serializeFilters(m_config->getTestsOrTags())); } if (m_config->rngSeed() != 0) { xml.scopedElement("property") .writeAttribute("name", "random-seed") .writeAttribute("value", m_config->rngSeed()); } } // Write test cases for( auto const& child : groupNode.children ) writeTestCase( *child ); xml.scopedElement( "system-out" ).writeText( trim( stdOutForSuite ), XmlFormatting::Newline ); xml.scopedElement( "system-err" ).writeText( trim( stdErrForSuite ), XmlFormatting::Newline ); } void JunitReporter::writeTestCase( TestCaseNode const& testCaseNode ) { TestCaseStats const& stats = testCaseNode.value; // All test cases have exactly one section - which represents the // test case itself. That section may have 0-n nested sections assert( testCaseNode.children.size() == 1 ); SectionNode const& rootSection = *testCaseNode.children.front(); std::string className = stats.testInfo.className; if( className.empty() ) { className = fileNameTag(stats.testInfo.tags); if ( className.empty() ) className = "global"; } if ( !m_config->name().empty() ) className = m_config->name() + "." + className; writeSection( className, "", rootSection ); } void JunitReporter::writeSection( std::string const& className, std::string const& rootName, SectionNode const& sectionNode ) { std::string name = trim( sectionNode.stats.sectionInfo.name ); if( !rootName.empty() ) name = rootName + '/' + name; if( !sectionNode.assertions.empty() || !sectionNode.stdOut.empty() || !sectionNode.stdErr.empty() ) { XmlWriter::ScopedElement e = xml.scopedElement( "testcase" ); if( className.empty() ) { xml.writeAttribute( "classname", name ); xml.writeAttribute( "name", "root" ); } else { xml.writeAttribute( "classname", className ); xml.writeAttribute( "name", name ); } xml.writeAttribute( "time", ::Catch::Detail::stringify( sectionNode.stats.durationInSeconds ) ); // This is not ideal, but it should be enough to mimic gtest's // junit output. // Ideally the JUnit reporter would also handle `skipTest` // events and write those out appropriately. xml.writeAttribute( "status", "run" ); writeAssertions( sectionNode ); if( !sectionNode.stdOut.empty() ) xml.scopedElement( "system-out" ).writeText( trim( sectionNode.stdOut ), XmlFormatting::Newline ); if( !sectionNode.stdErr.empty() ) xml.scopedElement( "system-err" ).writeText( trim( sectionNode.stdErr ), XmlFormatting::Newline ); } for( auto const& childNode : sectionNode.childSections ) if( className.empty() ) writeSection( name, "", *childNode ); else writeSection( className, name, *childNode ); } void JunitReporter::writeAssertions( SectionNode const& sectionNode ) { for( auto const& assertion : sectionNode.assertions ) writeAssertion( assertion ); } void JunitReporter::writeAssertion( AssertionStats const& stats ) { AssertionResult const& result = stats.assertionResult; if( !result.isOk() ) { std::string elementName; switch( result.getResultType() ) { case ResultWas::ThrewException: case ResultWas::FatalErrorCondition: elementName = "error"; break; case ResultWas::ExplicitFailure: case ResultWas::ExpressionFailed: case ResultWas::DidntThrowException: elementName = "failure"; break; // We should never see these here: case ResultWas::Info: case ResultWas::Warning: case ResultWas::Ok: case ResultWas::Unknown: case ResultWas::FailureBit: case ResultWas::Exception: elementName = "internalError"; break; } XmlWriter::ScopedElement e = xml.scopedElement( elementName ); xml.writeAttribute( "message", result.getExpression() ); xml.writeAttribute( "type", result.getTestMacroName() ); ReusableStringStream rss; if (stats.totals.assertions.total() > 0) { rss << "FAILED" << ":\n"; if (result.hasExpression()) { rss << " "; rss << result.getExpressionInMacro(); rss << '\n'; } if (result.hasExpandedExpression()) { rss << "with expansion:\n"; rss << Column(result.getExpandedExpression()).indent(2) << '\n'; } } else { rss << '\n'; } if( !result.getMessage().empty() ) rss << result.getMessage() << '\n'; for( auto const& msg : stats.infoMessages ) if( msg.type == ResultWas::Info ) rss << msg.message << '\n'; rss << "at " << result.getSourceInfo(); xml.writeText( rss.str(), XmlFormatting::Newline ); } } CATCH_REGISTER_REPORTER( "junit", JunitReporter ) } // end namespace Catch