From 0ab4432025683c4879484d6eaa8690e2e1d03db6 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Fri, 10 Dec 2010 08:01:42 +0000 Subject: [PATCH] First draft of Junit reporter --- Test/Test.xcodeproj/project.pbxproj | 6 +- catch_reporter_junit.hpp | 286 ++++++++++++++++++++++++++++ internal/catch_resultinfo.hpp | 3 + internal/catch_xmlwriter.hpp | 191 +++++++++++++++++++ 4 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 catch_reporter_junit.hpp create mode 100644 internal/catch_xmlwriter.hpp diff --git a/Test/Test.xcodeproj/project.pbxproj b/Test/Test.xcodeproj/project.pbxproj index 0731e547..f166bf8c 100644 --- a/Test/Test.xcodeproj/project.pbxproj +++ b/Test/Test.xcodeproj/project.pbxproj @@ -33,7 +33,8 @@ /* Begin PBXFileReference section */ 4A3BFFB8128DCF06005609E3 /* TestMain.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TestMain.cpp; sourceTree = ""; }; 4A3BFFF0128DD23C005609E3 /* catch_runnerconfig.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = catch_runnerconfig.hpp; path = ../internal/catch_runnerconfig.hpp; sourceTree = SOURCE_ROOT; }; - 4AA7E968129FA1DF005A0B97 /* catch_reporter_junit.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = catch_reporter_junit.hpp; path = ../../../Lib/Catch/catch_reporter_junit.hpp; sourceTree = SOURCE_ROOT; }; + 4A992A6512B2156C002B7B66 /* catch_xmlwriter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = catch_xmlwriter.hpp; path = ../internal/catch_xmlwriter.hpp; sourceTree = SOURCE_ROOT; }; + 4A992A6612B21582002B7B66 /* catch_reporter_junit.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = catch_reporter_junit.hpp; path = ../catch_reporter_junit.hpp; sourceTree = SOURCE_ROOT; }; 4AA7EA9112A438C7005A0B97 /* MiscTests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MiscTests.cpp; sourceTree = ""; }; 4AFC341512809A36003A0C29 /* catch_capture.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = catch_capture.hpp; path = ../internal/catch_capture.hpp; sourceTree = SOURCE_ROOT; }; 4AFC341612809A36003A0C29 /* catch_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = catch_common.h; path = ../internal/catch_common.h; sourceTree = SOURCE_ROOT; }; @@ -101,9 +102,9 @@ 4AA7E96B129FA282005A0B97 /* Reporters */ = { isa = PBXGroup; children = ( + 4A992A6612B21582002B7B66 /* catch_reporter_junit.hpp */, 4AFC341D12809A45003A0C29 /* catch_reporter_basic.hpp */, 4AFC341E12809A45003A0C29 /* catch_reporter_xml.hpp */, - 4AA7E968129FA1DF005A0B97 /* catch_reporter_junit.hpp */, ); name = Reporters; sourceTree = ""; @@ -136,6 +137,7 @@ 4AFC341412809A1B003A0C29 /* Internal */ = { isa = PBXGroup; children = ( + 4A992A6512B2156C002B7B66 /* catch_xmlwriter.hpp */, 4A3BFFF0128DD23C005609E3 /* catch_runnerconfig.hpp */, 4AFC341F12809A45003A0C29 /* catch_list.hpp */, 4AFC359B1281F00B003A0C29 /* catch_section.hpp */, diff --git a/catch_reporter_junit.hpp b/catch_reporter_junit.hpp new file mode 100644 index 00000000..3a27b746 --- /dev/null +++ b/catch_reporter_junit.hpp @@ -0,0 +1,286 @@ +/* + * catch_reporter_junit.hpp + * Catch + * + * 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) + * + */ +#ifndef TWOBLUECUBES_CATCH_REPORTER_JUNIT_HPP_INCLUDED +#define TWOBLUECUBES_CATCH_REPORTER_JUNIT_HPP_INCLUDED + +#include "internal/catch_capture.hpp" +#include "internal/catch_reporter_registry.hpp" +#include "internal/catch_xmlwriter.hpp" +#include + +namespace Catch +{ + class JunitReporter : public Catch::ITestReporter + { + struct Indenter + { + Indenter& operator ++() + { + m_indent += "\t"; + return *this; + } + Indenter& operator --() + { + m_indent = m_indent.substr( 0, m_indent.length()-1 ); + return *this; + } + + friend std::ostream& operator << ( std::ostream& os, const Indenter& indent ) + { + os << indent.m_indent; + return os; + } + + std::string m_indent; + }; + + struct TestStats + { + std::string element; + std::string resultType; + std::string message; + std::string content; + }; + + struct TestCaseStats + { + TestCaseStats( const std::string& name = std::string() ) + : name( name ) + { + } + + double timeInSeconds; + std::string status; + std::string className; + std::string name; + std::vector testStats; + }; + + struct Stats + { + Stats( const std::string& name = std::string() ) + : testsCount( 0 ), + failuresCount( 0 ), + disabledCount( 0 ), + errorsCount( 0 ), + timeInSeconds( 0 ), + name( name ) + { + } + + std::size_t testsCount; + std::size_t failuresCount; + std::size_t disabledCount; + std::size_t errorsCount; + double timeInSeconds; + std::string name; + + std::vector testCaseStats; + }; + + public: + /////////////////////////////////////////////////////////////////////////// + JunitReporter( const ReporterConfig& config = ReporterConfig() ) + : m_config( config ), + m_testSuiteStats( "AllTests" ), + m_currentStats( &m_testSuiteStats ) + { + } + + /////////////////////////////////////////////////////////////////////////// + static std::string getDescription() + { + return "Reports test results in an XML format that looks like Ant's junitreport target"; + } + + private: // ITestReporter + + /////////////////////////////////////////////////////////////////////////// + virtual void StartTesting() + { + } + + /////////////////////////////////////////////////////////////////////////// + virtual void StartGroup( const std::string& groupName ) + { + +// m_config.stream() << "\t\n"; +// if( !groupName.empty() ) + { + m_statsForSuites.push_back( Stats( groupName ) ); + m_currentStats = &m_statsForSuites.back(); + } + } + + /////////////////////////////////////////////////////////////////////////// + virtual void EndGroup( const std::string& groupName, std::size_t succeeded, std::size_t failed ) + { +// m_config.stream() << "\t\n"; + (groupName, succeeded, failed); + m_currentStats = &m_testSuiteStats; + } + + virtual void StartSection( const std::string& sectionName, const std::string description ){(sectionName,description);} + virtual void EndSection( const std::string& sectionName, std::size_t succeeded, std::size_t failed ){(sectionName, succeeded, failed);} + + /////////////////////////////////////////////////////////////////////////// + virtual void StartTestCase( const Catch::TestCaseInfo& testInfo ) + { +// m_config.stream() << "\t\t\n"; + // m_currentTestSuccess = true; + m_currentStats->testCaseStats.push_back( TestCaseStats( testInfo.getName() ) ); + + } + + /////////////////////////////////////////////////////////////////////////// + virtual void Result( const Catch::ResultInfo& resultInfo ) + { + if( !resultInfo.ok() || m_config.includeSuccessfulResults() ) + { + TestCaseStats& testCaseStats = m_currentStats->testCaseStats.back(); + TestStats stats; + std::ostringstream oss; + if( !resultInfo.getMessage().empty() ) + { + oss << resultInfo.getMessage() << " at "; + } + oss << resultInfo.getFilename() << ":" << resultInfo.getLine(); + stats.content = oss.str(); + stats.message = resultInfo.getExpandedExpression(); + stats.resultType = resultInfo.getTestMacroName(); + switch( resultInfo.getResultType() ) + { + case ResultWas::ThrewException: + stats.element = "error"; + break; + case ResultWas::Info: + stats.element = "info"; // !TBD ? + break; + case ResultWas::Warning: + stats.element = "warning"; // !TBD ? + break; + case ResultWas::ExplicitFailure: + stats.element = "failure"; + break; + case ResultWas::ExpressionFailed: + stats.element = "failure"; + break; + case ResultWas::Ok: + stats.element = "success"; + break; + default: + stats.element = "unknown"; + break; + } + testCaseStats.testStats.push_back( stats ); + + } + } + + /////////////////////////////////////////////////////////////////////////// + virtual void EndTestCase( const Catch::TestCaseInfo&, const std::string& stdOut, const std::string& stdErr ) + { + if( !stdOut.empty() ) + m_stdOut << stdOut << "\n"; + if( !stdErr.empty() ) + m_stdErr << stdErr << "\n"; + } + + static std::string trim( const std::string& str ) + { + std::string::size_type start = str.find_first_not_of( "\n\r\t " ); + std::string::size_type end = str.find_last_not_of( "\n\r\t " ); + + return start < end ? str.substr( start, 1+end-start ) : ""; + } + + /////////////////////////////////////////////////////////////////////////// + virtual void EndTesting( std::size_t /* succeeded */, std::size_t /* failed */ ) + { + std::ostream& str = m_config.stream(); + { + XmlWriter xml( str ); + + if( m_statsForSuites.size() > 0 ) + xml.startElement( "testsuites" ); + + std::vector::const_iterator it = m_statsForSuites.begin(); + std::vector::const_iterator itEnd = m_statsForSuites.end(); + + for(; it != itEnd; ++it ) + { + XmlWriter::ScopedElement e = xml.scopedElement( "testsuite" ); + xml.writeAttribute( "name", it->name ); + + OutputTestCases( xml, *it ); + } + + xml.scopedElement( "system-out" ).writeText( trim( m_stdOut.str() ) ); + xml.scopedElement( "system-err" ).writeText( trim( m_stdOut.str() ) ); + } + } + + /////////////////////////////////////////////////////////////////////////// + void OutputTestCases( XmlWriter& xml, const Stats& stats ) + { + std::vector::const_iterator it = stats.testCaseStats.begin(); + std::vector::const_iterator itEnd = stats.testCaseStats.end(); + for(; it != itEnd; ++it ) + { + xml.writeBlankLine(); + xml.writeComment( "Test case" ); + + XmlWriter::ScopedElement e = xml.scopedElement( "testcase" ); + xml.writeAttribute( "classname", it->className ); + xml.writeAttribute( "name", it->name ); + xml.writeAttribute( "time", "tbd" ); + + OutputTestResult( xml, *it ); + } + } + + + /////////////////////////////////////////////////////////////////////////// + void OutputTestResult( XmlWriter& xml, const TestCaseStats& stats ) + { + std::vector::const_iterator it = stats.testStats.begin(); + std::vector::const_iterator itEnd = stats.testStats.end(); + for(; it != itEnd; ++it ) + { + if( it->element != "success" ) + { + XmlWriter::ScopedElement e = xml.scopedElement( it->element ); + + xml.writeAttribute( "message", it->message ); + xml.writeAttribute( "type", it->resultType ); + if( !it->content.empty() ) + xml.writeText( it->content ); + } + } + } + + private: + const ReporterConfig& m_config; + bool m_currentTestSuccess; + + Stats m_testSuiteStats; + Stats* m_currentStats; + std::vector m_statsForSuites; + std::ostringstream m_stdOut; + std::ostringstream m_stdErr; + + Indenter m_indent; + }; + +} // end namespace Catch + +#endif // TWOBLUECUBES_CATCH_REPORTER_JUNIT_HPP_INCLUDED \ No newline at end of file diff --git a/internal/catch_resultinfo.hpp b/internal/catch_resultinfo.hpp index f7778cbf..dd99ea4d 100644 --- a/internal/catch_resultinfo.hpp +++ b/internal/catch_resultinfo.hpp @@ -82,6 +82,9 @@ namespace Catch } std::string getExpandedExpression() const { + if( !hasExpression() ) + return ""; + return m_expressionIncomplete ? getExpandedExpressionInternal() + " {can't expand the rest of the expression - consider rewriting it}" : getExpandedExpressionInternal(); diff --git a/internal/catch_xmlwriter.hpp b/internal/catch_xmlwriter.hpp new file mode 100644 index 00000000..ca73701b --- /dev/null +++ b/internal/catch_xmlwriter.hpp @@ -0,0 +1,191 @@ +/* + * catch_xmlwriter.hpp + * Catch + * + * Created by Phil on 09/12/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) + */ +#ifndef TWOBLUECUBES_CATCH_XMLWRITER_HPP_INCLUDED +#define TWOBLUECUBES_CATCH_XMLWRITER_HPP_INCLUDED + +namespace Catch +{ + class XmlWriter + { + public: + + class ScopedElement + { + public: + ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + { + } + + ScopedElement( const ScopedElement& other ) + : m_writer( other.m_writer ) + { + other.m_writer = NULL; + } + + ~ScopedElement() + { + if( m_writer ) + m_writer->endElement(); + } + + ScopedElement& writeText( const std::string& text ) + { + m_writer->writeText( text ); + return *this; + } + + private: + mutable XmlWriter* m_writer; + }; + + XmlWriter( std::ostream& os) + : m_tagIsOpen( false ), + m_needsNewline( false ), + m_os( os ) + { + } + + ~XmlWriter() + { + while( !m_tags.empty() ) + { + endElement(); + } + } + + XmlWriter& startElement( const std::string& name ) + { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << "<" << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + ScopedElement scopedElement( const std::string& name ) + { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& endElement() + { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) + { + m_os << "/>\n"; + m_tagIsOpen = false; + } + else + { + m_os << m_indent << "\n"; + } + m_tags.pop_back(); + return *this; + } + + XmlWriter& writeAttribute( const std::string& name, const std::string& attribute ) + { + if( !name.empty() && !attribute.empty() ) + { + m_os << " " << name << "=\""; + writeEncodedText( attribute ); + m_os << "\""; + } + return *this; + } + + XmlWriter& writeText( const std::string& text ) + { + if( !text.empty() ) + { + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen ) + m_os << m_indent; + writeEncodedText( text ); + m_needsNewline = true; + } + return *this; + } + + XmlWriter& writeComment( const std::string& text ) + { + ensureTagClosed(); + m_os << m_indent << ""; + m_needsNewline = true; + return *this; + } + + XmlWriter& writeBlankLine() + { + ensureTagClosed(); + m_os << "\n"; + return *this; + } + + private: + + void ensureTagClosed() + { + if( m_tagIsOpen ) + { + m_os << ">\n"; + m_tagIsOpen = false; + } + } + + void newlineIfNecessary() + { + if( m_needsNewline ) + { + m_os << "\n"; + m_needsNewline = false; + } + } + + void writeEncodedText( const std::string& text ) + { + // !TBD finish this + if( !findReplaceableString( text, "<", "<" ) && + !findReplaceableString( text, "&", "&" ) && + !findReplaceableString( text, "\"", ""e;" ) ) + { + m_os << text; + } + } + + bool findReplaceableString( const std::string& text, const std::string& replaceWhat, const std::string& replaceWith ) + { + std::string::size_type pos = text.find_first_of( replaceWhat ); + if( pos != std::string::npos ) + { + m_os << text.substr( 0, pos ) << replaceWith; + writeEncodedText( text.substr( pos+1 ) ); + return true; + } + return false; + } + + bool m_tagIsOpen; + bool m_needsNewline; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +} +#endif