summaryrefslogtreecommitdiff
path: root/indra/test
diff options
context:
space:
mode:
Diffstat (limited to 'indra/test')
-rw-r--r--indra/test/catch_and_store_what_in.h86
-rw-r--r--indra/test/llevents_tut.cpp78
-rw-r--r--indra/test/manageapr.h46
-rw-r--r--indra/test/namedtempfile.h205
-rw-r--r--indra/test/test.cpp313
5 files changed, 579 insertions, 149 deletions
diff --git a/indra/test/catch_and_store_what_in.h b/indra/test/catch_and_store_what_in.h
new file mode 100644
index 0000000000..59f8cc0085
--- /dev/null
+++ b/indra/test/catch_and_store_what_in.h
@@ -0,0 +1,86 @@
+/**
+ * @file catch_and_store_what_in.h
+ * @author Nat Goodspeed
+ * @date 2012-02-15
+ * @brief CATCH_AND_STORE_WHAT_IN() macro
+ *
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_CATCH_AND_STORE_WHAT_IN_H)
+#define LL_CATCH_AND_STORE_WHAT_IN_H
+
+/**
+ * Idiom useful for test programs: catch an expected exception, store its
+ * what() string in a specified std::string variable. From there the caller
+ * can do things like:
+ * @code
+ * ensure("expected exception not thrown", ! string.empty());
+ * @endcode
+ * or
+ * @code
+ * ensure_contains("exception doesn't mention blah", string, "blah");
+ * @endcode
+ * etc.
+ *
+ * The trouble is that when linking to a dynamic libllcommon.so on Linux, we
+ * generally fail to catch the specific exception. Oddly, we can catch it as
+ * std::runtime_error and validate its typeid().name(), so we do -- but that's
+ * a lot of boilerplate per test. Encapsulate with this macro. Usage:
+ *
+ * @code
+ * std::string threw;
+ * try
+ * {
+ * some_call_that_should_throw_Foo();
+ * }
+ * CATCH_AND_STORE_WHAT_IN(threw, Foo)
+ * ensure("some_call_that_should_throw_Foo() didn't throw", ! threw.empty());
+ * @endcode
+ */
+#define CATCH_AND_STORE_WHAT_IN(THREW, EXCEPTION) \
+catch (const EXCEPTION& ex) \
+{ \
+ (THREW) = ex.what(); \
+} \
+CATCH_MISSED_LINUX_EXCEPTION(THREW, EXCEPTION)
+
+#ifndef LL_LINUX
+#define CATCH_MISSED_LINUX_EXCEPTION(THREW, EXCEPTION) \
+ /* only needed on Linux */
+#else // LL_LINUX
+
+#define CATCH_MISSED_LINUX_EXCEPTION(THREW, EXCEPTION) \
+catch (const std::runtime_error& ex) \
+{ \
+ /* This clause is needed on Linux, on the viewer side, because */ \
+ /* the exception isn't caught by catch (const EXCEPTION&). */ \
+ /* But if the expected exception was thrown, allow the test to */ \
+ /* succeed anyway. Not sure how else to handle this odd case. */ \
+ if (std::string(typeid(ex).name()) == typeid(EXCEPTION).name()) \
+ { \
+ /* std::cerr << "Caught " << typeid(ex).name() */ \
+ /* << " with Linux workaround" << std::endl; */ \
+ (THREW) = ex.what(); \
+ /*std::cout << ex.what() << std::endl;*/ \
+ } \
+ else \
+ { \
+ /* We don't even recognize this exception. Let it propagate */ \
+ /* out to TUT to fail the test. */ \
+ throw; \
+ } \
+} \
+catch (...) \
+{ \
+ std::cerr << "Failed to catch expected exception " \
+ << #EXCEPTION << "!" << std::endl; \
+ /* This indicates a problem in the test that should be addressed. */ \
+ throw; \
+}
+
+#endif // LL_LINUX
+
+#endif /* ! defined(LL_CATCH_AND_STORE_WHAT_IN_H) */
diff --git a/indra/test/llevents_tut.cpp b/indra/test/llevents_tut.cpp
index 4699bb1827..a9114075fc 100644
--- a/indra/test/llevents_tut.cpp
+++ b/indra/test/llevents_tut.cpp
@@ -49,46 +49,12 @@
#include <boost/assign/list_of.hpp>
// other Linden headers
#include "lltut.h"
+#include "catch_and_store_what_in.h"
#include "stringize.h"
#include "tests/listener.h"
using boost::assign::list_of;
-#ifdef LL_LINUX
-#define CATCH_MISSED_LINUX_EXCEPTION(exception, threw) \
-catch (const std::runtime_error& ex) \
-{ \
- /* This clause is needed on Linux, on the viewer side, because the */ \
- /* exception isn't caught by the clause above. Warn the user... */ \
- std::cerr << "Failed to catch " << typeid(ex).name() << std::endl; \
- /* But if the expected exception was thrown, allow the test to */ \
- /* succeed anyway. Not sure how else to handle this odd case. */ \
- /* This approach is also used in llsdmessage_test.cpp. */ \
- if (std::string(typeid(ex).name()) == typeid(exception).name()) \
- { \
- threw = ex.what(); \
- /*std::cout << ex.what() << std::endl;*/ \
- } \
- else \
- { \
- /* We don't even recognize this exception. Let it propagate */ \
- /* out to TUT to fail the test. */ \
- throw; \
- } \
-} \
-catch (...) \
-{ \
- std::cerr << "Utterly failed to catch expected exception " << #exception << "!" << \
- std::endl; \
- /* This indicates a problem in the test that should be addressed. */ \
- throw; \
-}
-
-#else // LL_LINUX
-#define CATCH_MISSED_LINUX_EXCEPTION(exception, threw) \
- /* Not needed on other platforms */
-#endif // LL_LINUX
-
template<typename T>
T make(const T& value)
{
@@ -178,11 +144,7 @@ void events_object::test<1>()
per_frame.listen(listener0.getName(), // note bug, dup name
boost::bind(&Listener::call, boost::ref(listener1), _1));
}
- catch (const LLEventPump::DupListenerName& e)
- {
- threw = e.what();
- }
- CATCH_MISSED_LINUX_EXCEPTION(LLEventPump::DupListenerName, threw)
+ CATCH_AND_STORE_WHAT_IN(threw, LLEventPump::DupListenerName)
ensure_equals(threw,
std::string("DupListenerName: "
"Attempt to register duplicate listener name '") +
@@ -354,7 +316,6 @@ void events_object::test<7>()
{
set_test_name("listener dependency order");
typedef LLEventPump::NameList NameList;
- typedef Collect::StringList StringList;
LLEventPump& button(pumps.obtain("button"));
Collect collector;
button.listen("Mary",
@@ -368,7 +329,7 @@ void events_object::test<7>()
button.listen("spot",
boost::bind(&Collect::add, boost::ref(collector), "spot", _1));
button.post(1);
- ensure_equals(collector.result, make<StringList>(list_of("spot")("checked")("Mary")));
+ ensure_equals(collector.result, make<StringVec>(list_of("spot")("checked")("Mary")));
collector.clear();
button.stopListening("Mary");
button.listen("Mary",
@@ -377,7 +338,7 @@ void events_object::test<7>()
// now "Mary" must come before "spot"
make<NameList>(list_of("spot")));
button.post(2);
- ensure_equals(collector.result, make<StringList>(list_of("Mary")("spot")("checked")));
+ ensure_equals(collector.result, make<StringVec>(list_of("Mary")("spot")("checked")));
collector.clear();
button.stopListening("spot");
std::string threw;
@@ -388,12 +349,7 @@ void events_object::test<7>()
// after "Mary" and "checked" -- whoops!
make<NameList>(list_of("Mary")("checked")));
}
- catch (const LLEventPump::Cycle& e)
- {
- threw = e.what();
- // std::cout << "Caught: " << e.what() << '\n';
- }
- CATCH_MISSED_LINUX_EXCEPTION(LLEventPump::Cycle, threw)
+ CATCH_AND_STORE_WHAT_IN(threw, LLEventPump::Cycle)
// Obviously the specific wording of the exception text can
// change; go ahead and change the test to match.
// Establish that it contains:
@@ -416,7 +372,7 @@ void events_object::test<7>()
boost::bind(&Collect::add, boost::ref(collector), "shoelaces", _1),
make<NameList>(list_of("checked")));
button.post(3);
- ensure_equals(collector.result, make<StringList>(list_of("Mary")("checked")("yellow")("shoelaces")));
+ ensure_equals(collector.result, make<StringVec>(list_of("Mary")("checked")("yellow")("shoelaces")));
collector.clear();
threw.clear();
try
@@ -426,12 +382,7 @@ void events_object::test<7>()
make<NameList>(list_of("shoelaces")),
make<NameList>(list_of("yellow")));
}
- catch (const LLEventPump::OrderChange& e)
- {
- threw = e.what();
- // std::cout << "Caught: " << e.what() << '\n';
- }
- CATCH_MISSED_LINUX_EXCEPTION(LLEventPump::OrderChange, threw)
+ CATCH_AND_STORE_WHAT_IN(threw, LLEventPump::OrderChange)
// Same remarks about the specific wording of the exception. Just
// ensure that it contains enough information to clarify the
// problem and what must be done to resolve it.
@@ -443,7 +394,7 @@ void events_object::test<7>()
ensure_contains("old order", threw, "was: Mary, checked, yellow, shoelaces");
ensure_contains("new order", threw, "now: Mary, checked, shoelaces, of, yellow");
button.post(4);
- ensure_equals(collector.result, make<StringList>(list_of("Mary")("checked")("yellow")("shoelaces")));
+ ensure_equals(collector.result, make<StringVec>(list_of("Mary")("checked")("yellow")("shoelaces")));
}
template<> template<>
@@ -459,12 +410,7 @@ void events_object::test<8>()
// then another with a duplicate name.
LLEventStream bob2("bob");
}
- catch (const LLEventPump::DupPumpName& e)
- {
- threw = e.what();
- // std::cout << "Caught: " << e.what() << '\n';
- }
- CATCH_MISSED_LINUX_EXCEPTION(LLEventPump::DupPumpName, threw)
+ CATCH_AND_STORE_WHAT_IN(threw, LLEventPump::DupPumpName)
ensure("Caught DupPumpName", !threw.empty());
} // delete first 'bob'
LLEventStream bob("bob"); // should work, previous one unregistered
@@ -505,11 +451,7 @@ void events_object::test<9>()
LLListenerOrPumpName empty;
empty(17);
}
- catch (const LLListenerOrPumpName::Empty& e)
- {
- threw = e.what();
- }
- CATCH_MISSED_LINUX_EXCEPTION(LLListenerOrPumpName::Empty, threw)
+ CATCH_AND_STORE_WHAT_IN(threw, LLListenerOrPumpName::Empty)
ensure("threw Empty", !threw.empty());
}
diff --git a/indra/test/manageapr.h b/indra/test/manageapr.h
new file mode 100644
index 0000000000..2452fb6ae4
--- /dev/null
+++ b/indra/test/manageapr.h
@@ -0,0 +1,46 @@
+/**
+ * @file manageapr.h
+ * @author Nat Goodspeed
+ * @date 2012-01-13
+ * @brief ManageAPR class for simple test programs
+ *
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_MANAGEAPR_H)
+#define LL_MANAGEAPR_H
+
+#include "llapr.h"
+#include <boost/noncopyable.hpp>
+
+/**
+ * Declare a static instance of this class for dead-simple ll_init_apr() at
+ * program startup, ll_cleanup_apr() at termination. This is recommended for
+ * use only with simple test programs. Once you start introducing static
+ * instances of other classes that depend on APR already being initialized,
+ * the indeterminate static-constructor-order problem rears its ugly head.
+ */
+class ManageAPR: public boost::noncopyable
+{
+public:
+ ManageAPR()
+ {
+ ll_init_apr();
+ }
+
+ ~ManageAPR()
+ {
+ ll_cleanup_apr();
+ }
+
+ static std::string strerror(apr_status_t rv)
+ {
+ char errbuf[256];
+ apr_strerror(rv, errbuf, sizeof(errbuf));
+ return errbuf;
+ }
+};
+
+#endif /* ! defined(LL_MANAGEAPR_H) */
diff --git a/indra/test/namedtempfile.h b/indra/test/namedtempfile.h
new file mode 100644
index 0000000000..6069064627
--- /dev/null
+++ b/indra/test/namedtempfile.h
@@ -0,0 +1,205 @@
+/**
+ * @file namedtempfile.h
+ * @author Nat Goodspeed
+ * @date 2012-01-13
+ * @brief NamedTempFile class for tests that need disk files as fixtures.
+ *
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_NAMEDTEMPFILE_H)
+#define LL_NAMEDTEMPFILE_H
+
+#include "llerror.h"
+#include "llapr.h"
+#include "apr_file_io.h"
+#include <string>
+#include <boost/function.hpp>
+#include <boost/lambda/lambda.hpp>
+#include <boost/lambda/bind.hpp>
+#include <boost/noncopyable.hpp>
+#include <iostream>
+#include <sstream>
+
+/**
+ * Create a text file with specified content "somewhere in the
+ * filesystem," cleaning up when it goes out of scope.
+ */
+class NamedTempFile: public boost::noncopyable
+{
+ LOG_CLASS(NamedTempFile);
+public:
+ NamedTempFile(const std::string& pfx, const std::string& content, apr_pool_t* pool=gAPRPoolp):
+ mPool(pool)
+ {
+ createFile(pfx, boost::lambda::_1 << content);
+ }
+
+ // Disambiguate when passing string literal
+ NamedTempFile(const std::string& pfx, const char* content, apr_pool_t* pool=gAPRPoolp):
+ mPool(pool)
+ {
+ createFile(pfx, boost::lambda::_1 << content);
+ }
+
+ // Function that accepts an ostream ref and (presumably) writes stuff to
+ // it, e.g.:
+ // (boost::lambda::_1 << "the value is " << 17 << '\n')
+ typedef boost::function<void(std::ostream&)> Streamer;
+
+ NamedTempFile(const std::string& pfx, const Streamer& func, apr_pool_t* pool=gAPRPoolp):
+ mPool(pool)
+ {
+ createFile(pfx, func);
+ }
+
+ virtual ~NamedTempFile()
+ {
+ ll_apr_assert_status(apr_file_remove(mPath.c_str(), mPool));
+ }
+
+ virtual std::string getName() const { return mPath; }
+
+ void peep()
+ {
+ std::cout << "File '" << mPath << "' contains:\n";
+ std::ifstream reader(mPath.c_str());
+ std::string line;
+ while (std::getline(reader, line))
+ std::cout << line << '\n';
+ std::cout << "---\n";
+ }
+
+protected:
+ void createFile(const std::string& pfx, const Streamer& func)
+ {
+ // Create file in a temporary place.
+ const char* tempdir = NULL;
+ ll_apr_assert_status(apr_temp_dir_get(&tempdir, mPool));
+
+ // Construct a temp filename template in that directory.
+ char *tempname = NULL;
+ ll_apr_assert_status(apr_filepath_merge(&tempname,
+ tempdir,
+ (pfx + "XXXXXX").c_str(),
+ 0,
+ mPool));
+
+ // Create a temp file from that template.
+ apr_file_t* fp = NULL;
+ ll_apr_assert_status(apr_file_mktemp(&fp,
+ tempname,
+ APR_CREATE | APR_WRITE | APR_EXCL,
+ mPool));
+ // apr_file_mktemp() alters tempname with the actual name. Not until
+ // now is it valid to capture as our mPath.
+ mPath = tempname;
+
+ // Write desired content.
+ std::ostringstream out;
+ // Stream stuff to it.
+ func(out);
+
+ std::string data(out.str());
+ apr_size_t writelen(data.length());
+ ll_apr_assert_status(apr_file_write(fp, data.c_str(), &writelen));
+ ll_apr_assert_status(apr_file_close(fp));
+ llassert_always(writelen == data.length());
+ }
+
+ std::string mPath;
+ apr_pool_t* mPool;
+};
+
+/**
+ * Create a NamedTempFile with a specified filename extension. This is useful
+ * when, for instance, you must be able to use the file in a Python import
+ * statement.
+ *
+ * A NamedExtTempFile actually has two different names. We retain the original
+ * no-extension name as a placeholder in the temp directory to ensure
+ * uniqueness; to that we link the name plus the desired extension. Naturally,
+ * both must be removed on destruction.
+ */
+class NamedExtTempFile: public NamedTempFile
+{
+ LOG_CLASS(NamedExtTempFile);
+public:
+ NamedExtTempFile(const std::string& ext, const std::string& content, apr_pool_t* pool=gAPRPoolp):
+ NamedTempFile(remove_dot(ext), content, pool),
+ mLink(mPath + ensure_dot(ext))
+ {
+ linkto(mLink);
+ }
+
+ // Disambiguate when passing string literal
+ NamedExtTempFile(const std::string& ext, const char* content, apr_pool_t* pool=gAPRPoolp):
+ NamedTempFile(remove_dot(ext), content, pool),
+ mLink(mPath + ensure_dot(ext))
+ {
+ linkto(mLink);
+ }
+
+ NamedExtTempFile(const std::string& ext, const Streamer& func, apr_pool_t* pool=gAPRPoolp):
+ NamedTempFile(remove_dot(ext), func, pool),
+ mLink(mPath + ensure_dot(ext))
+ {
+ linkto(mLink);
+ }
+
+ virtual ~NamedExtTempFile()
+ {
+ ll_apr_assert_status(apr_file_remove(mLink.c_str(), mPool));
+ }
+
+ // Since the caller has gone to the trouble to create the name with the
+ // extension, that should be the name we return. In this class, mPath is
+ // just a placeholder to ensure that future createFile() calls won't
+ // collide.
+ virtual std::string getName() const { return mLink; }
+
+ static std::string ensure_dot(const std::string& ext)
+ {
+ if (ext.empty())
+ {
+ // What SHOULD we do when the caller makes a point of using
+ // NamedExtTempFile to generate a file with a particular
+ // extension, then passes an empty extension? Use just "."? That
+ // sounds like a Bad Idea, especially on Windows. Treat that as a
+ // coding error.
+ LL_ERRS("NamedExtTempFile") << "passed empty extension" << LL_ENDL;
+ }
+ if (ext[0] == '.')
+ {
+ return ext;
+ }
+ return std::string(".") + ext;
+ }
+
+ static std::string remove_dot(const std::string& ext)
+ {
+ std::string::size_type found = ext.find_first_not_of(".");
+ if (found == std::string::npos)
+ {
+ return ext;
+ }
+ return ext.substr(found);
+ }
+
+private:
+ void linkto(const std::string& path)
+ {
+ // This method assumes that since mPath (without extension) is
+ // guaranteed by apr_file_mktemp() to be unique, then (mPath + any
+ // extension) is also unique. This is likely, though not guaranteed:
+ // files could be created in the same temp directory other than by
+ // this class.
+ ll_apr_assert_status(apr_file_link(mPath.c_str(), path.c_str()));
+ }
+
+ std::string mLink;
+};
+
+#endif /* ! defined(LL_NAMEDTEMPFILE_H) */
diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index ffdb0cb976..128d84e428 100644
--- a/indra/test/test.cpp
+++ b/indra/test/test.cpp
@@ -37,6 +37,8 @@
#include "linden_common.h"
#include "llerrorcontrol.h"
#include "lltut.h"
+#include "stringize.h"
+#include "namedtempfile.h"
#include "apr_pools.h"
#include "apr_getopt.h"
@@ -53,24 +55,118 @@
#include <gtest/gtest.h>
#endif
+#if LL_MSVC
+#pragma warning (push)
+#pragma warning (disable : 4702) // warning C4702: unreachable code
+#endif
+#include <boost/iostreams/tee.hpp>
+#include <boost/iostreams/stream.hpp>
+#if LL_MSVC
+#pragma warning (pop)
+#endif
+
+#include <boost/shared_ptr.hpp>
+#include <boost/make_shared.hpp>
+#include <boost/foreach.hpp>
+#include <boost/lambda/lambda.hpp>
+
+#include <fstream>
+
+void wouldHaveCrashed(const std::string& message);
+
namespace tut
{
std::string sSourceDir;
-
- test_runner_singleton runner;
+
+ test_runner_singleton runner;
}
+class LLReplayLog
+{
+public:
+ LLReplayLog() {}
+ virtual ~LLReplayLog() {}
+
+ virtual void reset() {}
+ virtual void replay(std::ostream&) {}
+};
+
+class LLReplayLogReal: public LLReplayLog, public LLError::Recorder
+{
+public:
+ LLReplayLogReal(LLError::ELevel level, apr_pool_t* pool):
+ mOldSettings(LLError::saveAndResetSettings()),
+ mTempFile("log", "", pool), // create file
+ mFile(mTempFile.getName().c_str()) // open it
+ {
+ LLError::setFatalFunction(wouldHaveCrashed);
+ LLError::setDefaultLevel(level);
+ LLError::addRecorder(this);
+ }
+
+ virtual ~LLReplayLogReal()
+ {
+ LLError::removeRecorder(this);
+ LLError::restoreSettings(mOldSettings);
+ }
+
+ virtual void recordMessage(LLError::ELevel level, const std::string& message)
+ {
+ mFile << message << std::endl;
+ }
+
+ virtual void reset()
+ {
+ mFile.close();
+ mFile.open(mTempFile.getName().c_str());
+ }
+
+ virtual void replay(std::ostream& out)
+ {
+ mFile.close();
+ std::ifstream inf(mTempFile.getName().c_str());
+ std::string line;
+ while (std::getline(inf, line))
+ {
+ out << line << std::endl;
+ }
+ }
+
+private:
+ LLError::Settings* mOldSettings;
+ NamedTempFile mTempFile;
+ std::ofstream mFile;
+};
+
class LLTestCallback : public tut::callback
{
public:
- LLTestCallback(bool verbose_mode, std::ostream *stream) :
+ LLTestCallback(bool verbose_mode, std::ostream *stream,
+ boost::shared_ptr<LLReplayLog> replayer) :
mVerboseMode(verbose_mode),
mTotalTests(0),
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)),
+ mReplayer(replayer)
{
+ 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()
@@ -93,8 +189,21 @@ public:
virtual void test_completed(const tut::test_result& tr)
{
++mTotalTests;
+
+ // If this test failed, dump requested log messages BEFORE stating the
+ // test result.
+ if (tr.result != tut::test_result::ok && tr.result != tut::test_result::skip)
+ {
+ mReplayer->replay(*mStream);
+ }
+ // Either way, clear stored messages in preparation for next test.
+ mReplayer->reset();
+
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 +232,43 @@ public:
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 << "'";
- }
- if (mStream)
- {
- *mStream << out.str() << std::endl;
+ *mStream << ": '" << tr.message << "'";
}
-
- 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 +278,8 @@ protected:
int mPassedTests;
int mFailedTests;
int mSkippedTests;
- std::ostream *mStream;
+ boost::shared_ptr<std::ostream> mStream;
+ boost::shared_ptr<LLReplayLog> mReplayer;
};
// TeamCity specific class which emits service messages
@@ -191,85 +288,113 @@ protected:
class LLTCTestCallback : public LLTestCallback
{
public:
- LLTCTestCallback(bool verbose_mode, std::ostream *stream) :
- LLTestCallback(verbose_mode, stream),
- mTCStream()
+ LLTCTestCallback(bool verbose_mode, std::ostream *stream,
+ boost::shared_ptr<LLReplayLog> replayer) :
+ LLTestCallback(verbose_mode, stream, replayer)
{
}
~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()
+ static std::string escape(const std::string& str)
{
- LLTestCallback::run_completed();
-
- // dump the TC reporting results to cout
- tc_run_completed_(std::cout);
- }
-
- virtual void tc_run_completed_(std::ostream &stream)
- {
-
- // 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;
-
};
@@ -306,6 +431,14 @@ void stream_usage(std::ostream& s, const char* app)
++option;
}
+ s << app << " is also sensitive to environment variables:\n"
+ << "LOGTEST=level : for all tests, emit log messages at level 'level'\n"
+ << "LOGFAIL=level : only for failed tests, emit log messages at level 'level'\n"
+ << "where 'level' is one of ALL, DEBUG, INFO, WARN, ERROR, NONE.\n"
+ << "--debug is like LOGTEST=DEBUG, but --debug overrides LOGTEST.\n"
+ << "Setting LOGFAIL overrides both LOGTEST and --debug: the only log\n"
+ << "messages you will see will be for failed tests.\n\n";
+
s << "Examples:" << std::endl;
s << " " << app << " --verbose" << std::endl;
s << "\tRun all the tests and report all results." << std::endl;
@@ -342,8 +475,14 @@ int main(int argc, char **argv)
LLError::initForApplication(".");
LLError::setFatalFunction(wouldHaveCrashed);
LLError::setDefaultLevel(LLError::LEVEL_ERROR);
- //< *TODO: should come from error config file. Note that we
- // have a command line option that sets this to debug.
+ // ^ possibly overridden by --debug, LOGTEST or LOGFAIL
+
+ // LOGTEST overrides default, but can be overridden by --debug or LOGFAIL.
+ const char* LOGTEST = getenv("LOGTEST");
+ if (LOGTEST)
+ {
+ LLError::setDefaultLevel(LLError::decodeLevel(LOGTEST));
+ }
#ifdef CTYPE_WORKAROUND
ctype_workaround();
@@ -418,8 +557,6 @@ int main(int argc, char **argv)
wait_at_exit = true;
break;
case 'd':
- // *TODO: should come from error config file. We set it to
- // ERROR by default, so this allows full debug levels.
LLError::setDefaultLevel(LLError::LEVEL_DEBUG);
break;
case 'x':
@@ -434,14 +571,28 @@ int main(int argc, char **argv)
// run the tests
+ const char* LOGFAIL = getenv("LOGFAIL");
+ boost::shared_ptr<LLReplayLog> replayer;
+ // As described in stream_usage(), LOGFAIL overrides both --debug and
+ // LOGTEST.
+ if (LOGFAIL)
+ {
+ LLError::ELevel level = LLError::decodeLevel(LOGFAIL);
+ replayer.reset(new LLReplayLogReal(level, pool));
+ }
+ else
+ {
+ replayer.reset(new LLReplayLog());
+ }
+
LLTestCallback* mycallback;
if (getenv("TEAMCITY_PROJECT_NAME"))
{
- mycallback = new LLTCTestCallback(verbose_mode, output);
+ mycallback = new LLTCTestCallback(verbose_mode, output, replayer);
}
else
{
- mycallback = new LLTestCallback(verbose_mode, output);
+ mycallback = new LLTestCallback(verbose_mode, output, replayer);
}
tut::runner.get().set_callback(mycallback);