From 90ba675da4416a7f75f59340633e2c007b6cc029 Mon Sep 17 00:00:00 2001
From: Nat Goodspeed <nat@lindenlab.com>
Date: Fri, 3 Feb 2012 13:09:20 -0500
Subject: [PATCH] Escape all strings embedded in TeamCity service messages.
 TeamCity requires that certain characters (notably "'") must be escaped when
 embedded in service messages:
 http://confluence.jetbrains.net/display/TCD65/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ServiceMessages
 TUT frequently outputs messages containing "'", e.g. from ensure_equals()
 failure. We've seen TC output nesting get confused when it fails to process
 service messages properly due to parsing unescaped messages. Along with
 test<n> number, report test name (from set_test_name()) when available.
 Eliminate horsing around to produce normal output on both std::cout and
 possible output file. When output file is specified, use
 boost::iostreams::tee_device to do fanout for us. Improve placement (and
 possibly reliability) of service messages. Clean up a startling amount of
 redundancy in service-message production.

---
 indra/test/test.cpp | 183 +++++++++++++++++++++++++++-----------------
 1 file changed, 112 insertions(+), 71 deletions(-)

diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index ffdb0cb976a..3e7be29b390 100644
--- a/indra/test/test.cpp
+++ b/indra/test/test.cpp
@@ -37,6 +37,7 @@
 #include "linden_common.h"
 #include "llerrorcontrol.h"
 #include "lltut.h"
+#include "stringize.h"
 
 #include "apr_pools.h"
 #include "apr_getopt.h"
@@ -53,6 +54,13 @@
 #include <gtest/gtest.h>
 #endif
 
+#include <boost/iostreams/tee.hpp>
+#include <boost/iostreams/stream.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/make_shared.hpp>
+#include <boost/foreach.hpp>
+#include <boost/lambda/lambda.hpp>
+
 namespace tut
 {
 	std::string sSourceDir;
@@ -69,8 +77,24 @@ class LLTestCallback : public tut::callback
 	mPassedTests(0),
 	mFailedTests(0),
 	mSkippedTests(0),
-	mStream(stream)
+	// By default, capture a shared_ptr to std::cout, with a no-op "deleter"
+	// so that destroying the shared_ptr makes no attempt to delete std::cout.
+	mStream(boost::shared_ptr<std::ostream>(&std::cout, boost::lambda::_1))
 	{
+		if (stream)
+		{
+			// We want a boost::iostreams::tee_device that will stream to two
+			// std::ostreams.
+			typedef boost::iostreams::tee_device<std::ostream, std::ostream> TeeDevice;
+			// More than that, though, we want an actual stream using that
+			// device.
+			typedef boost::iostreams::stream<TeeDevice> TeeStream;
+			// Allocate and assign in two separate steps, per Herb Sutter.
+			// (Until we turn on C++11 support, have to wrap *stream with
+			// boost::ref() due to lack of perfect forwarding.)
+			boost::shared_ptr<std::ostream> pstream(new TeeStream(std::cout, boost::ref(*stream)));
+			mStream = pstream;
+		}
 	}
 
 	~LLTestCallback()
@@ -94,7 +118,10 @@ class LLTestCallback : public tut::callback
 	{
 		++mTotalTests;
 		std::ostringstream out;
-		out << "[" << tr.group << ", " << tr.test << "] ";
+		out << "[" << tr.group << ", " << tr.test;
+		if (! tr.name.empty())
+			out << ": " << tr.name;
+		out << "] ";
 		switch(tr.result)
 		{
 			case tut::test_result::ok:
@@ -123,56 +150,43 @@ class LLTestCallback : public tut::callback
 				break;
 			default:
 				++mFailedTests;
-				out << "unknown";
+				out << "unknown (tr.result == " << tr.result << ")";
 		}
 		if(mVerboseMode || (tr.result != tut::test_result::ok))
 		{
+			*mStream << out.str();
 			if(!tr.message.empty())
 			{
-				out << ": '" << tr.message << "'";
+				*mStream << ": '" << tr.message << "'";
 			}
-			if (mStream)
-			{
-				*mStream << out.str() << std::endl;
-			}
-			
-			std::cout << out.str() << std::endl;
-		}
-	}
-
-	virtual void run_completed()
-	{
-		if (mStream)
-		{
-			run_completed_(*mStream);
+			*mStream << std::endl;
 		}
-		run_completed_(std::cout);
 	}
 
 	virtual int getFailedTests() const { return mFailedTests; }
 
-	virtual void run_completed_(std::ostream &stream)
+	virtual void run_completed()
 	{
-		stream << "\tTotal Tests:\t" << mTotalTests << std::endl;
-		stream << "\tPassed Tests:\t" << mPassedTests;
+		*mStream << "\tTotal Tests:\t" << mTotalTests << std::endl;
+		*mStream << "\tPassed Tests:\t" << mPassedTests;
 		if (mPassedTests == mTotalTests)
 		{
-			stream << "\tYAY!! \\o/";
+			*mStream << "\tYAY!! \\o/";
 		}
-		stream << std::endl;
+		*mStream << std::endl;
 
 		if (mSkippedTests > 0)
 		{
-			stream << "\tSkipped known failures:\t" << mSkippedTests
+			*mStream << "\tSkipped known failures:\t" << mSkippedTests
 			<< std::endl;
 		}
 
 		if(mFailedTests > 0)
 		{
-			stream << "*********************************" << std::endl;
-			stream << "Failed Tests:\t" << mFailedTests << std::endl;
-			stream << "Please report or fix the problem." << std::endl;
-			stream << "*********************************" << std::endl;
+			*mStream << "*********************************" << std::endl;
+			*mStream << "Failed Tests:\t" << mFailedTests << std::endl;
+			*mStream << "Please report or fix the problem." << std::endl;
+			*mStream << "*********************************" << std::endl;
 		}
 	}
 
@@ -182,7 +196,7 @@ class LLTestCallback : public tut::callback
 	int mPassedTests;
 	int mFailedTests;
 	int mSkippedTests;
-	std::ostream *mStream;
+	boost::shared_ptr<std::ostream> mStream;
 };
 
 // TeamCity specific class which emits service messages
@@ -192,84 +206,111 @@ class LLTCTestCallback : public LLTestCallback
 {
 public:
 	LLTCTestCallback(bool verbose_mode, std::ostream *stream) :
-		LLTestCallback(verbose_mode, stream),
-		mTCStream()
+		LLTestCallback(verbose_mode, stream)
 	{
 	}
 
 	~LLTCTestCallback()
 	{
-	}	
+	}
 
 	virtual void group_started(const std::string& name) {
 		LLTestCallback::group_started(name);
-		mTCStream << "\n##teamcity[testSuiteStarted name='" << name << "']" << std::endl;
+		std::cout << "\n##teamcity[testSuiteStarted name='" << escape(name) << "']" << std::endl;
 	}
 
 	virtual void group_completed(const std::string& name) {
 		LLTestCallback::group_completed(name);
-		mTCStream << "##teamcity[testSuiteFinished name='" << name << "']" << std::endl;
+		std::cout << "##teamcity[testSuiteFinished name='" << escape(name) << "']" << std::endl;
 	}
 
 	virtual void test_completed(const tut::test_result& tr)
 	{
+		std::string testname(STRINGIZE(tr.group << "." << tr.test));
+		if (! tr.name.empty())
+		{
+			testname.append(":");
+			testname.append(tr.name);
+		}
+		testname = escape(testname);
+
+		// Sadly, tut::callback doesn't give us control at test start; have to
+		// backfill start message into TC output.
+		std::cout << "##teamcity[testStarted name='" << testname << "']" << std::endl;
+
+		// now forward call to base class so any output produced there is in
+		// the right TC context
 		LLTestCallback::test_completed(tr);
 
 		switch(tr.result)
 		{
 			case tut::test_result::ok:
-				mTCStream << "##teamcity[testStarted name='" << tr.group << "." << tr.test << "']" << std::endl;
-				mTCStream << "##teamcity[testFinished name='" << tr.group << "." << tr.test << "']" << std::endl;
 				break;
+
 			case tut::test_result::fail:
-				mTCStream << "##teamcity[testStarted name='" << tr.group << "." << tr.test << "']" << std::endl;
-				mTCStream << "##teamcity[testFailed name='" << tr.group << "." << tr.test << "' message='" << tr.message << "']" << std::endl;
-				mTCStream << "##teamcity[testFinished name='" << tr.group << "." << tr.test << "']" << std::endl;
-				break;
 			case tut::test_result::ex:
-				mTCStream << "##teamcity[testStarted name='" << tr.group << "." << tr.test << "']" << std::endl;
-				mTCStream << "##teamcity[testFailed name='" << tr.group << "." << tr.test << "' message='" << tr.message << "']" << std::endl;
-				mTCStream << "##teamcity[testFinished name='" << tr.group << "." << tr.test << "']" << std::endl;
-				break;
 			case tut::test_result::warn:
-				mTCStream << "##teamcity[testStarted name='" << tr.group << "." << tr.test << "']" << std::endl;
-				mTCStream << "##teamcity[testFailed name='" << tr.group << "." << tr.test << "' message='" << tr.message << "']" << std::endl;
-				mTCStream << "##teamcity[testFinished name='" << tr.group << "." << tr.test << "']" << std::endl;
-				break;
 			case tut::test_result::term:
-				mTCStream << "##teamcity[testStarted name='" << tr.group << "." << tr.test << "']" << std::endl;
-				mTCStream << "##teamcity[testFailed name='" << tr.group << "." << tr.test << "' message='" << tr.message << "']" << std::endl;
-				mTCStream << "##teamcity[testFinished name='" << tr.group << "." << tr.test << "']" << std::endl;
+				std::cout << "##teamcity[testFailed name='" << testname
+						  << "' message='" << escape(tr.message) << "']" << std::endl;
 				break;
+
 			case tut::test_result::skip:
-				mTCStream << "##teamcity[testStarted name='" << tr.group << "." << tr.test << "']" << std::endl;
-				mTCStream << "##teamcity[testIgnored name='" << tr.group << "." << tr.test << "']" << std::endl;
-				mTCStream << "##teamcity[testFinished name='" << tr.group << "." << tr.test << "']" << std::endl;
+				std::cout << "##teamcity[testIgnored name='" << testname << "']" << std::endl;
 				break;
+
 			default:
 				break;
 		}
 
+		std::cout << "##teamcity[testFinished name='" << testname << "']" << std::endl;
 	}
 
-	virtual void run_completed()
-	{
-		LLTestCallback::run_completed();
-
-		// dump the TC reporting results to cout
-		tc_run_completed_(std::cout);
-	}
-
-	virtual void tc_run_completed_(std::ostream &stream)
+	static std::string escape(const std::string& str)
 	{
-		
-		// dump the TC reporting results to cout
-		stream << mTCStream.str() << std::endl;
+		// Per http://confluence.jetbrains.net/display/TCD65/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-ServiceMessages
+		std::string result;
+		BOOST_FOREACH(char c, str)
+		{
+			switch (c)
+			{
+			case '\'':
+				result.append("|'");
+				break;
+			case '\n':
+				result.append("|n");
+				break;
+			case '\r':
+				result.append("|r");
+				break;
+/*==========================================================================*|
+			// These are not possible 'char' values from a std::string.
+			case '\u0085':			// next line
+				result.append("|x");
+				break;
+			case '\u2028':			// line separator
+				result.append("|l");
+				break;
+			case '\u2029':			// paragraph separator
+				result.append("|p");
+				break;
+|*==========================================================================*/
+			case '|':
+				result.append("||");
+				break;
+			case '[':
+				result.append("|[");
+				break;
+			case ']':
+				result.append("|]");
+				break;
+			default:
+				result.push_back(c);
+				break;
+			}
+		}
+		return result;
 	}
-	
-protected:
-	std::ostringstream mTCStream;
-
 };
 
 
-- 
GitLab