diff --git a/indra/llcommon/llerror.h b/indra/llcommon/llerror.h
index 48162eca9eb8f609769ba47fa976dafd38338517..3cdd051ac73c60285612c2b92d90a0981e13e6e5 100644
--- a/indra/llcommon/llerror.h
+++ b/indra/llcommon/llerror.h
@@ -191,9 +191,9 @@ namespace LLError
 		The classes CallSite and Log are used by the logging macros below.
 		They are not intended for general use.
 	*/
-	
+
 	struct CallSite;
-	
+
 	class LL_COMMON_API Log
 	{
 	public:
@@ -202,8 +202,17 @@ namespace LLError
 		static void flush(std::ostringstream* out, char* message);
 		static void flush(std::ostringstream*, const CallSite&);
 		static std::string demangle(const char* mangled);
+		/// classname<TYPE>()
+		template <typename T>
+		static std::string classname()             { return demangle(typeid(T).name()); }
+		/// classname(some_pointer)
+		template <typename T>
+		static std::string classname(const T* ptr) { return demangle(typeid(*ptr).name()); }
+		/// classname(some_reference)
+		template <typename T>
+		static std::string classname(const T& obj) { return demangle(typeid(obj).name()); }
 	};
-	
+
 	struct LL_COMMON_API CallSite
 	{
 		// Represents a specific place in the code where a message is logged
diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h
index 27c2ceb3b614b2f2928e2cbd3267bb3c78a58a0e..ccd2e48bf2966e5445a73cf5d34f309dda678951 100644
--- a/indra/llcommon/llsingleton.h
+++ b/indra/llcommon/llsingleton.h
@@ -120,6 +120,8 @@ class LLSingletonBase: private boost::noncopyable
     static void logdebugs(const char* p1, const char* p2="",
                           const char* p3="", const char* p4="");
     static std::string demangle(const char* mangled);
+    // these classname() declarations restate template functions declared in
+    // llerror.h because we avoid #including that here
     template <typename T>
     static std::string classname()       { return demangle(typeid(T).name()); }
     template <typename T>
diff --git a/indra/llcommon/tests/lleventdispatcher_test.cpp b/indra/llcommon/tests/lleventdispatcher_test.cpp
index efb75951beea23d9f374a08ae592ba60171664b9..9da1ecfd67a71fc87cf889f118c986e6b7a79ffe 100644
--- a/indra/llcommon/tests/lleventdispatcher_test.cpp
+++ b/indra/llcommon/tests/lleventdispatcher_test.cpp
@@ -23,6 +23,7 @@
 #include "stringize.h"
 #include "tests/wrapllerrs.h"
 #include "../test/catch_and_store_what_in.h"
+#include "../test/debug.h"
 
 #include <map>
 #include <string>
@@ -45,15 +46,6 @@ using boost::lambda::var;
 
 using namespace llsd;
 
-/*****************************************************************************
-*   Output control
-*****************************************************************************/
-#ifdef DEBUG_ON
-using std::cout;
-#else
-static std::ostringstream cout;
-#endif
-
 /*****************************************************************************
 *   Example data, functions, classes
 *****************************************************************************/
@@ -155,13 +147,13 @@ struct Vars
     /*------------- no-args (non-const, const, static) methods -------------*/
     void method0()
     {
-        cout << "method0()\n";
+        debug()("method0()");
         i = 17;
     }
 
     void cmethod0() const
     {
-        cout << 'c';
+        debug()('c', NONL);
         const_cast<Vars*>(this)->method0();
     }
 
@@ -170,13 +162,13 @@ struct Vars
     /*------------ Callable (non-const, const, static) methods -------------*/
     void method1(const LLSD& obj)
     {
-        cout << "method1(" << obj << ")\n";
+        debug()("method1(", obj, ")");
         llsd = obj;
     }
 
     void cmethod1(const LLSD& obj) const
     {
-        cout << 'c';
+        debug()('c', NONL);
         const_cast<Vars*>(this)->method1(obj);
     }
 
@@ -196,12 +188,12 @@ struct Vars
         else
             vcp = std::string("'") + cp + "'";
 
-        cout << "methodna(" << b
-             << ", " << i
-             << ", " << f
-             << ", " << d
-             << ", " << vcp
-             << ")\n";
+        debug()("methodna(", b,
+              ", ", i,
+              ", ", f,
+              ", ", d,
+              ", ", vcp,
+              ")");
 
         this->b = b;
         this->i = i;
@@ -218,12 +210,12 @@ struct Vars
             vbin << std::hex << std::setfill('0') << std::setw(2) << unsigned(byte);
         }
 
-        cout << "methodnb(" << "'" << s << "'"
-             << ", " << uuid
-             << ", " << date
-             << ", '" << uri << "'"
-             << ", " << vbin.str()
-             << ")\n";
+        debug()("methodnb(", "'", s, "'",
+              ", ", uuid,
+              ", ", date,
+              ", '", uri, "'",
+              ", ", vbin.str(),
+              ")");
 
         this->s = s;
         this->uuid = uuid;
@@ -234,18 +226,30 @@ struct Vars
 
     void cmethodna(NPARAMSa) const
     {
-        cout << 'c';
+        debug()('c', NONL);
         const_cast<Vars*>(this)->methodna(NARGSa);
     }
 
     void cmethodnb(NPARAMSb) const
     {
-        cout << 'c';
+        debug()('c', NONL);
         const_cast<Vars*>(this)->methodnb(NARGSb);
     }
 
     static void smethodna(NPARAMSa);
     static void smethodnb(NPARAMSb);
+
+    static Debug& debug()
+    {
+        // Lazily initialize this Debug instance so it can notice if main()
+        // has forcibly set LOGTEST. If it were simply a static member, it
+        // would already have examined the environment variable by the time
+        // main() gets around to checking command-line switches. Since we have
+        // a global static Vars instance, the same would be true of a plain
+        // non-static member.
+        static Debug sDebug("Vars");
+        return sDebug;
+    }
 };
 /*------- Global Vars instance for free functions and static methods -------*/
 static Vars g;
@@ -253,25 +257,25 @@ static Vars g;
 /*------------ Static Vars method implementations reference 'g' ------------*/
 void Vars::smethod0()
 {
-    cout << "smethod0() -> ";
+    debug()("smethod0() -> ", NONL);
     g.method0();
 }
 
 void Vars::smethod1(const LLSD& obj)
 {
-    cout << "smethod1(" << obj << ") -> ";
+    debug()("smethod1(", obj, ") -> ", NONL);
     g.method1(obj);
 }
 
 void Vars::smethodna(NPARAMSa)
 {
-    cout << "smethodna(...) -> ";
+    debug()("smethodna(...) -> ", NONL);
     g.methodna(NARGSa);
 }
 
 void Vars::smethodnb(NPARAMSb)
 {
-    cout << "smethodnb(...) -> ";
+    debug()("smethodnb(...) -> ", NONL);
     g.methodnb(NARGSb);
 }
 
@@ -284,25 +288,25 @@ void clear()
 /*------------------- Free functions also reference 'g' --------------------*/
 void free0()
 {
-    cout << "free0() -> ";
+    g.debug()("free0() -> ", NONL);
     g.method0();
 }
 
 void free1(const LLSD& obj)
 {
-    cout << "free1(" << obj << ") -> ";
+    g.debug()("free1(", obj, ") -> ", NONL);
     g.method1(obj);
 }
 
 void freena(NPARAMSa)
 {
-    cout << "freena(...) -> ";
+    g.debug()("freena(...) -> ", NONL);
     g.methodna(NARGSa);
 }
 
 void freenb(NPARAMSb)
 {
-    cout << "freenb(...) -> ";
+    g.debug()("freenb(...) -> ", NONL);
     g.methodnb(NARGSb);
 }
 
@@ -313,6 +317,7 @@ namespace tut
 {
     struct lleventdispatcher_data
     {
+        Debug debug{"test"};
         WrapLLErrs redirect;
         Dispatcher work;
         Vars v;
@@ -431,7 +436,12 @@ namespace tut
             // Same for freenb() et al.
             params = LLSDMap("a", LLSDArray("b")("i")("f")("d")("cp"))
                             ("b", LLSDArray("s")("uuid")("date")("uri")("bin"));
-            cout << "params:\n" << params << "\nparams[\"a\"]:\n" << params["a"] << "\nparams[\"b\"]:\n" << params["b"] << std::endl;
+            debug("params:\n",
+                  params, "\n"
+                  "params[\"a\"]:\n",
+                  params["a"], "\n"
+                  "params[\"b\"]:\n",
+                  params["b"]);
             // default LLSD::Binary value   
             std::vector<U8> binary;
             for (size_t ix = 0, h = 0xaa; ix < 6; ++ix, h += 0x11)
@@ -448,7 +458,8 @@ namespace tut
                                                    (LLDate::now())
                                                    (LLURI("http://www.ietf.org/rfc/rfc3986.txt"))
                                                    (binary));
-            cout << "dft_array_full:\n" << dft_array_full << std::endl;
+            debug("dft_array_full:\n",
+                  dft_array_full);
             // Partial defaults arrays.
             foreach(LLSD::String a, ab)
             {
@@ -457,7 +468,8 @@ namespace tut
                     llsd_copy_array(dft_array_full[a].beginArray() + partition,
                                     dft_array_full[a].endArray());
             }
-            cout << "dft_array_partial:\n" << dft_array_partial << std::endl;
+            debug("dft_array_partial:\n",
+                  dft_array_partial);
 
             foreach(LLSD::String a, ab)
             {
@@ -473,7 +485,10 @@ namespace tut
                     dft_map_partial[a][params[a][ix].asString()] = dft_array_full[a][ix];
                 }
             }
-            cout << "dft_map_full:\n" << dft_map_full << "\ndft_map_partial:\n" << dft_map_partial << '\n';
+            debug("dft_map_full:\n",
+                  dft_map_full, "\n"
+                  "dft_map_partial:\n",
+                  dft_map_partial);
 
             // (Free function | static method) with (no | arbitrary) params,
             // map style, no (empty array) defaults
@@ -918,7 +933,12 @@ namespace tut
                                                  params[a].endArray()),
                                  dft_array_partial[a]);
         }
-        cout << "allreq:\n" << allreq << "\nleftreq:\n" << leftreq << "\nrightdft:\n" << rightdft << std::endl;
+        debug("allreq:\n",
+              allreq, "\n"
+              "leftreq:\n",
+              leftreq, "\n"
+              "rightdft:\n",
+              rightdft);
 
         // Generate maps containing parameter names not provided by the
         // dft_map_partial maps.
@@ -930,7 +950,8 @@ namespace tut
                 skipreq[a].erase(me.first);
             }
         }
-        cout << "skipreq:\n" << skipreq << std::endl;
+        debug("skipreq:\n",
+              skipreq);
 
         LLSD groups(LLSDArray       // array of groups
 
@@ -975,7 +996,11 @@ namespace tut
             LLSD names(grp[0]);
             LLSD required(grp[1][0]);
             LLSD optional(grp[1][1]);
-            cout << "For " << names << ",\n" << "required:\n" << required << "\noptional:\n" << optional << std::endl;
+            debug("For ", names, ",\n",
+                  "required:\n",
+                  required, "\n"
+                  "optional:\n",
+                  optional);
 
             // Loop through 'names'
             foreach(LLSD nm, inArray(names))
@@ -1163,7 +1188,7 @@ namespace tut
         }
         // Adjust expect["a"]["cp"] for special Vars::cp treatment.
         expect["a"]["cp"] = std::string("'") + expect["a"]["cp"].asString() + "'";
-        cout << "expect: " << expect << '\n';
+        debug("expect: ", expect);
 
         // Use substantially the same logic for args and argsplus
         LLSD argsarrays(LLSDArray(args)(argsplus));
@@ -1218,7 +1243,8 @@ namespace tut
         {
             array_overfull[a].append("bogus");
         }
-        cout << "array_full: " << array_full << "\narray_overfull: " << array_overfull << std::endl;
+        debug("array_full: ", array_full, "\n"
+              "array_overfull: ", array_overfull);
         // We rather hope that LLDate::now() will generate a timestamp
         // distinct from the one it generated in the constructor, moments ago.
         ensure_not_equals("Timestamps too close",
@@ -1233,7 +1259,8 @@ namespace tut
             map_overfull[a] = map_full[a];
             map_overfull[a]["extra"] = "ignore";
         }
-        cout << "map_full: " << map_full << "\nmap_overfull: " << map_overfull << std::endl;
+        debug("map_full: ", map_full, "\n"
+              "map_overfull: ", map_overfull);
         LLSD expect(map_full);
         // Twiddle the const char* param.
         expect["a"]["cp"] = std::string("'") + expect["a"]["cp"].asString() + "'";
@@ -1248,7 +1275,7 @@ namespace tut
         // so won't bother returning it. Predict that behavior to match the
         // LLSD values.
         expect["a"].erase("b");
-        cout << "expect: " << expect << std::endl;
+        debug("expect: ", expect);
         // For this test, calling functions registered with different sets of
         // parameter defaults should make NO DIFFERENCE WHATSOEVER. Every call
         // should pass all params.
diff --git a/indra/test/chained_callback.h b/indra/test/chained_callback.h
new file mode 100644
index 0000000000000000000000000000000000000000..41a7f7c9fa53d920b30af598d8be1ce4fb8b2afa
--- /dev/null
+++ b/indra/test/chained_callback.h
@@ -0,0 +1,105 @@
+/**
+ * @file   chained_callback.h
+ * @author Nat Goodspeed
+ * @date   2020-01-03
+ * @brief  Subclass of tut::callback used for chaining callbacks.
+ * 
+ * $LicenseInfo:firstyear=2020&license=viewerlgpl$
+ * Copyright (c) 2020, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_CHAINED_CALLBACK_H)
+#define LL_CHAINED_CALLBACK_H
+
+/**
+ * Derive your TUT callback from chained_callback instead of tut::callback to
+ * ensure that multiple such callbacks can coexist in a given test executable.
+ * The relevant callback method will be called for each callback instance in
+ * reverse order of the instance's link() methods being called: the most
+ * recently link()ed callback will be called first, then the previous, and so
+ * forth.
+ *
+ * Obviously, for this to work, all relevant callbacks must be derived from
+ * chained_callback instead of tut::callback. Given that, control should reach
+ * each of them regardless of their construction order. The chain is
+ * guaranteed to stop because the first link() call will link to test_runner's
+ * default_callback, which is simply an instance of the callback() base class.
+ *
+ * The rule for deriving from chained_callback is that you may override any of
+ * its virtual methods, but your override must at some point call the
+ * corresponding chained_callback method.
+ */
+class chained_callback: public tut::callback
+{
+public:
+    /**
+     * Instead of calling tut::test_runner::set_callback(&your_callback), call
+     * your_callback.link();
+     * This uses the canonical instance of tut::test_runner.
+     */
+    void link()
+    {
+        link(tut::runner.get());
+    }
+
+    /**
+     * If for some reason you have a different instance of test_runner...
+     */
+    void link(tut::test_runner& runner)
+    {
+        // Since test_runner's constructor sets a default callback,
+        // get_callback() will always return a reference to a valid callback
+        // instance.
+        mPrev = &runner.get_callback();
+        runner.set_callback(this);
+    }
+
+    /**
+     * Called when new test run started.
+     */
+    virtual void run_started()
+    {
+        mPrev->run_started();
+    }
+
+    /**
+     * Called when a group started
+     * @param name Name of the group
+     */
+    virtual void group_started(const std::string& name)
+    {
+        mPrev->group_started(name);
+    }
+
+    /**
+     * Called when a test finished.
+     * @param tr Test results.
+     */
+    virtual void test_completed(const tut::test_result& tr)
+    {
+        mPrev->test_completed(tr);
+    }
+
+    /**
+     * Called when a group is completed
+     * @param name Name of the group
+     */
+    virtual void group_completed(const std::string& name)
+    {
+        mPrev->group_completed(name);
+    }
+
+    /**
+     * Called when all tests in run completed.
+     */
+    virtual void run_completed()
+    {
+        mPrev->run_completed();
+    }
+
+private:
+    tut::callback* mPrev;
+};
+
+#endif /* ! defined(LL_CHAINED_CALLBACK_H) */
diff --git a/indra/test/debug.h b/indra/test/debug.h
index 33c3ea2d27eb49c81059f9eba7e452d51df3e5ca..76dbb973b2b870827edcbca5f49dfd167cc9c165 100644
--- a/indra/test/debug.h
+++ b/indra/test/debug.h
@@ -29,37 +29,59 @@
 #if ! defined(LL_DEBUG_H)
 #define LL_DEBUG_H
 
-#include <iostream>
+#include "print.h"
 
 /*****************************************************************************
 *   Debugging stuff
 *****************************************************************************/
-// This class is intended to illuminate entry to a given block, exit from the
-// same block and checkpoints along the way. It also provides a convenient
-// place to turn std::cout output on and off.
+/**
+ * This class is intended to illuminate entry to a given block, exit from the
+ * same block and checkpoints along the way. It also provides a convenient
+ * place to turn std::cerr output on and off.
+ *
+ * If the environment variable LOGTEST is non-empty, each Debug instance will
+ * announce its construction and destruction, presumably at entry and exit to
+ * the block in which it's declared. Moreover, any arguments passed to its
+ * operator()() will be streamed to std::cerr, prefixed by the block
+ * description.
+ *
+ * The variable LOGTEST is used because that's the environment variable
+ * checked by test.cpp, our TUT main() program, to turn on LLError logging. It
+ * is expected that Debug is solely for use in test programs.
+ */
 class Debug
 {
 public:
     Debug(const std::string& block):
-        mBlock(block)
+        mBlock(block),
+        mLOGTEST(getenv("LOGTEST")),
+        // debug output enabled when LOGTEST is set AND non-empty
+        mEnabled(mLOGTEST && *mLOGTEST)
     {
         (*this)("entry");
     }
 
+    // non-copyable
+    Debug(const Debug&) = delete;
+
     ~Debug()
     {
         (*this)("exit");
     }
 
-    void operator()(const std::string& status)
+    template <typename... ARGS>
+    void operator()(ARGS&&... args)
     {
-#if defined(DEBUG_ON)
-        std::cout << mBlock << ' ' << status << std::endl;
-#endif
+        if (mEnabled)
+        {
+            print(mBlock, ' ', std::forward<ARGS>(args)...);
+        }
     }
 
 private:
     const std::string mBlock;
+    const char* mLOGTEST;
+    bool mEnabled;
 };
 
 // It's often convenient to use the name of the enclosing function as the name
diff --git a/indra/test/print.h b/indra/test/print.h
new file mode 100644
index 0000000000000000000000000000000000000000..08e36caddffb10f1098644612c493a81c27a4c97
--- /dev/null
+++ b/indra/test/print.h
@@ -0,0 +1,42 @@
+/**
+ * @file   print.h
+ * @author Nat Goodspeed
+ * @date   2020-01-02
+ * @brief  print() function for debugging
+ * 
+ * $LicenseInfo:firstyear=2020&license=viewerlgpl$
+ * Copyright (c) 2020, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_PRINT_H)
+#define LL_PRINT_H
+
+#include <iostream>
+
+// print(..., NONL);
+// leaves the output dangling, suppressing the normally appended std::endl
+struct NONL_t {};
+#define NONL (NONL_t())
+
+// normal recursion end
+inline
+void print()
+{
+    std::cerr << std::endl;
+}
+
+// print(NONL) is a no-op
+inline
+void print(NONL_t)
+{
+}
+
+template <typename T, typename... ARGS>
+void print(T&& first, ARGS&&... rest)
+{
+    std::cerr << first;
+    print(std::forward<ARGS>(rest)...);
+}
+
+#endif /* ! defined(LL_PRINT_H) */
diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index 6b342ffe89ad4028624b7a4c89a1ce423695b6d7..ea54ba658e1385d833c3ead14c0af59af68547d8 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 "chained_callback.h"
 #include "stringize.h"
 #include "namedtempfile.h"
 #include "lltrace.h"
@@ -71,7 +72,6 @@
 #include <boost/shared_ptr.hpp>
 #include <boost/make_shared.hpp>
 #include <boost/foreach.hpp>
-#include <boost/lambda/lambda.hpp>
 
 #include <fstream>
 
@@ -172,8 +172,10 @@ class LLReplayLogReal: public LLReplayLog, public boost::noncopyable
 	LLError::RecorderPtr mRecorder;
 };
 
-class LLTestCallback : public tut::callback
+class LLTestCallback : public chained_callback
 {
+	typedef chained_callback super;
+
 public:
 	LLTestCallback(bool verbose_mode, std::ostream *stream,
 				   boost::shared_ptr<LLReplayLog> replayer) :
@@ -184,7 +186,7 @@ class LLTestCallback : public tut::callback
 		mSkippedTests(0),
 		// 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)),
+		mStream(boost::shared_ptr<std::ostream>(&std::cout, [](std::ostream*){})),
 		mReplayer(replayer)
 	{
 		if (stream)
@@ -205,22 +207,25 @@ class LLTestCallback : public tut::callback
 
 	~LLTestCallback()
 	{
-	}	
+	}
 
 	virtual void run_started()
 	{
 		//std::cout << "run_started" << std::endl;
 		LL_INFOS("TestRunner")<<"Test Started"<< LL_ENDL;
+		super::run_started();
 	}
 
 	virtual void group_started(const std::string& name) {
 		LL_INFOS("TestRunner")<<"Unit test group_started name=" << name << LL_ENDL;
 		*mStream << "Unit test group_started name=" << name << std::endl;
+		super::group_started(name);
 	}
 
 	virtual void group_completed(const std::string& name) {
 		LL_INFOS("TestRunner")<<"Unit test group_completed name=" << name << LL_ENDL;
 		*mStream << "Unit test group_completed name=" << name << std::endl;
+		super::group_completed(name);
 	}
 
 	virtual void test_completed(const tut::test_result& tr)
@@ -282,6 +287,7 @@ class LLTestCallback : public tut::callback
 			*mStream << std::endl;
 		}
 		LL_INFOS("TestRunner")<<out.str()<<LL_ENDL;
+		super::test_completed(tr);
 	}
 
 	virtual int getFailedTests() const { return mFailedTests; }
@@ -309,6 +315,7 @@ class LLTestCallback : public tut::callback
 			*mStream << "Please report or fix the problem." << std::endl;
 			*mStream << "*********************************" << std::endl;
 		}
+		super::run_completed();
 	}
 
 protected:
@@ -474,9 +481,8 @@ void stream_usage(std::ostream& s, const char* app)
 	  << "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";
+	  << "--debug is like LOGTEST=DEBUG, but --debug overrides LOGTEST,\n"
+	  << "while LOGTEST overrides LOGFAIL.\n\n";
 
 	s << "Examples:" << std::endl;
 	s << "  " << app << " --verbose" << std::endl;
@@ -520,35 +526,8 @@ int main(int argc, char **argv)
 #ifndef LL_WINDOWS
 	::testing::InitGoogleMock(&argc, argv);
 #endif
-	// LOGTEST overrides default, but can be overridden by --debug or LOGFAIL.
-	const char* LOGTEST = getenv("LOGTEST");
-	if (LOGTEST)
-	{
-		LLError::initForApplication(".", ".", true /* log to stderr */);
-		LLError::setDefaultLevel(LLError::decodeLevel(LOGTEST));
-	}
-	else
-	{
-		LLError::initForApplication(".", ".", false /* do not log to stderr */);
-		LLError::setDefaultLevel(LLError::LEVEL_DEBUG);
-	}	
-	LLError::setFatalFunction(wouldHaveCrashed);
-	std::string test_app_name(argv[0]);
-	std::string test_log = test_app_name + ".log";
-	LLFile::remove(test_log);
-	LLError::logToFile(test_log);
-
-#ifdef CTYPE_WORKAROUND
-	ctype_workaround();
-#endif
 
 	ll_init_apr();
-	
-	if (!sMasterThreadRecorder)
-	{
-		sMasterThreadRecorder = new LLTrace::ThreadRecorder();
-		LLTrace::set_master_thread_recorder(sMasterThreadRecorder);
-	}
 	apr_getopt_t* os = NULL;
 	if(APR_SUCCESS != apr_getopt_init(&os, gAPRPoolp, argc, argv))
 	{
@@ -562,7 +541,13 @@ int main(int argc, char **argv)
 	std::string test_group;
 	std::string suite_name;
 
-	// values use for options parsing
+	// LOGTEST overrides default, but can be overridden by --debug.
+	const char* LOGTEST = getenv("LOGTEST");
+
+    // DELETE
+    LOGTEST = "DEBUG";
+
+	// values used for options parsing
 	apr_status_t apr_err;
 	const char* opt_arg = NULL;
 	int opt_id = 0;
@@ -611,10 +596,7 @@ int main(int argc, char **argv)
 				wait_at_exit = true;
 				break;
 			case 'd':
-				// this is what LLError::initForApplication() does internally
-				// when you pass log_to_stderr=true
-				LLError::logToStderr();
-				LLError::setDefaultLevel(LLError::LEVEL_DEBUG);
+				LOGTEST = "DEBUG";
 				break;
 			case 'x':
 				suite_name.assign(opt_arg);
@@ -626,22 +608,44 @@ int main(int argc, char **argv)
 		}
 	}
 
-	// run the tests
-
+	// set up logging
 	const char* LOGFAIL = getenv("LOGFAIL");
-	boost::shared_ptr<LLReplayLog> replayer;
-	// As described in stream_usage(), LOGFAIL overrides both --debug and
-	// LOGTEST. But allow user to set LOGFAIL empty to revert to LOGTEST
-	// and/or --debug.
-	if (LOGFAIL && *LOGFAIL)
+	boost::shared_ptr<LLReplayLog> replayer{boost::make_shared<LLReplayLog>()};
+
+	// Testing environment variables for both 'set' and 'not empty' allows a
+	// user to suppress a pre-existing environment variable by forcing empty.
+	if (LOGTEST && *LOGTEST)
 	{
-		LLError::ELevel level = LLError::decodeLevel(LOGFAIL);
-		replayer.reset(new LLReplayLogReal(level, gAPRPoolp));
+		LLError::initForApplication(".", ".", true /* log to stderr */);
+		LLError::setDefaultLevel(LLError::decodeLevel(LOGTEST));
 	}
 	else
 	{
-		replayer.reset(new LLReplayLog());
+		LLError::initForApplication(".", ".", false /* do not log to stderr */);
+		LLError::setDefaultLevel(LLError::LEVEL_DEBUG);
+		if (LOGFAIL && *LOGFAIL)
+		{
+			LLError::ELevel level = LLError::decodeLevel(LOGFAIL);
+			replayer.reset(new LLReplayLogReal(level, gAPRPoolp));
+		}
 	}
+	LLError::setFatalFunction(wouldHaveCrashed);
+	std::string test_app_name(argv[0]);
+	std::string test_log = test_app_name + ".log";
+	LLFile::remove(test_log);
+	LLError::logToFile(test_log);
+
+#ifdef CTYPE_WORKAROUND
+	ctype_workaround();
+#endif
+
+	if (!sMasterThreadRecorder)
+	{
+		sMasterThreadRecorder = new LLTrace::ThreadRecorder();
+		LLTrace::set_master_thread_recorder(sMasterThreadRecorder);
+	}
+
+	// run the tests
 
 	LLTestCallback* mycallback;
 	if (getenv("TEAMCITY_PROJECT_NAME"))
@@ -653,7 +657,8 @@ int main(int argc, char **argv)
 		mycallback = new LLTestCallback(verbose_mode, output.get(), replayer);
 	}
 
-	tut::runner.get().set_callback(mycallback);
+	// a chained_callback subclass must be linked with previous
+	mycallback->link();
 
 	if(test_group.empty())
 	{
diff --git a/indra/viewer_components/login/tests/lllogin_test.cpp b/indra/viewer_components/login/tests/lllogin_test.cpp
index 0255e10e53341e437b5ea9bcc4281871e981169b..0d933a3d64ba0aa1f628d56027b05229ecebc124 100644
--- a/indra/viewer_components/login/tests/lllogin_test.cpp
+++ b/indra/viewer_components/login/tests/lllogin_test.cpp
@@ -43,7 +43,6 @@
 #include "llsd.h"
 #include "../../../test/lltut.h"
 #include "../../../test/lltestapp.h"
-//#define DEBUG_ON
 #include "../../../test/debug.h"
 #include "llevents.h"
 #include "lleventcoro.h"