diff options
Diffstat (limited to 'indra/test')
-rw-r--r-- | indra/test/catch_and_store_what_in.h | 86 | ||||
-rw-r--r-- | indra/test/llevents_tut.cpp | 78 | ||||
-rw-r--r-- | indra/test/manageapr.h | 46 | ||||
-rw-r--r-- | indra/test/namedtempfile.h | 205 | ||||
-rw-r--r-- | indra/test/test.cpp | 313 |
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); |