Commits

Matt Oswald committed 9bd0742

adding color to the standard console reporter's output

Comments (0)

Files changed (30)

Tests/BareTests/BareTest.cpp

 // This is the simplest valid test library possible, and is intended to ensure
 // this error does not happen in the future.
 
-FACT("BareTest")
+FACT("Bare test")
 {
 }

Tests/UnitTests/Assert.Contains.cpp

     std::vector<int> v;
 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Contains(v, 0) << msg; });
-    auto what = std::string(assert.what());
+    auto what = std::string(assert.UserMessage());
 
     Assert.NotEqual(std::string::npos, what.find(msg));
 }
     std::string actual = "abcd";
 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Contains(actual, "xyz") << msg; });
-    auto what = std::string(assert.what());
+    auto what = std::string(assert.UserMessage());
 
     Assert.NotEqual(std::string::npos, what.find(msg));
 }

Tests/UnitTests/Assert.DoesNotContain.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.DoesNotContain(v, 0) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 FACT("DoesNotContainForStringSuccess")
 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.DoesNotContain(actual, "ab") << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.DoesNotThrow.cpp

     }
 }
 
-FACT("DoesNotThrowAssertsOnFailure")
+FACT("DoesNotThrow should assert when something is thrown")
 {
     static const std::string msg = "xUnit++";
 
     Assert.Fail();
 }
 
-FACT("DoesNotThrowAppendsMessages")
+FACT("DoesNotThrow should append any user messages")
 {
     static const std::string exceptionMessage = "xUnit++";
     static const std::string userMessage = "custom";
     }
     catch(const xUnitAssert &assert)
     {
-        Assert.Contains(assert.what(), exceptionMessage.c_str());
-        Assert.Contains(assert.what(), userMessage.c_str());
+        Assert.Contains(assert.Actual(), exceptionMessage.c_str());
+        Assert.Contains(assert.UserMessage(), userMessage.c_str());
         return;
     }
 

Tests/UnitTests/Assert.Empty.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Empty(v) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.Equal.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([]() { Assert.Equal(0, 1) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 FACT("AssertEqualForFloatsWithinPrecision")
     Assert.Equal(v0.begin(), v0.end(), v1.begin(), v1.end());
 }
 
-FACT("AssertSequenceEqualDefaultComparerAssertsOnFailureDueToLength")
+FACT("SequenceEqual should assert with differing lengths")
 {
     std::vector<int> v0;
     v0.push_back(0);
 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Equal(v0.begin(), v0.end(), v1.begin(), v1.end()); });
 
-    Assert.Contains(assert.what(), "at location 2");
+    Assert.Contains(assert.CustomMessage(), "at location 2");
 }
 
-FACT("AssertSequenceEqualDefaultComparerAssertsOnFailureDueToMismatch")
+FACT("SequenceEqual should assert due to element mismatch")
 {
     std::vector<int> v0;
     v0.push_back(0);
 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Equal(v0.begin(), v0.end(), v1.begin(), v1.end()); });
 
-    Assert.Contains(assert.what(), "at location 1");
+    Assert.Contains(assert.CustomMessage(), "at location 1");
 }
 
 FACT("AssertSequenceEqualCustomComparerWithSuccess")
     Assert.Equal(v0.begin(), v0.end(), v1.begin(), v1.end(), [](int, long long) { return true; });
 }
 
-FACT("AssertSequenceEqualCustomComparerAssertsOnFailureDueToLength")
+FACT("SequenceEqual should assert with differing lengths (custom comparer)")
 {
     std::vector<int> v0;
     v0.push_back(0);
 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Equal(v0.begin(), v0.end(), v1.begin(), v1.end(), [](int, long long) { return true; }); });
 
-    Assert.Contains(assert.what(), "at location 2");
+    Assert.Contains(assert.CustomMessage(), "at location 2");
 }
 
-FACT("AssertSequenceEqualDefaultAssertsOnFailureDueToMismatch")
+FACT("SequenceEqual should assert due to element mismatch (custom comparer)")
 {
     std::vector<int> v0;
     v0.push_back(0);
 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Equal(v0.begin(), v0.end(), v1.begin(), v1.end(), [](int, long long) { return false; }); });
 
-    Assert.Contains(assert.what(), "at location 0");
+    Assert.Contains(assert.CustomMessage(), "at location 0");
 }
 
 FACT("EqualForStrings")

Tests/UnitTests/Assert.Fail.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([]() { Assert.Fail() << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.False.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([]() { Assert.False(true) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.InRange.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([=]() { Assert.InRange(1, 0, 1) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 FACT("InRangeNeedsValidRange")

Tests/UnitTests/Assert.NotEqual.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([]() { Assert.NotEqual(0, 0) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 FACT("AssertSequenceNotEqualDefaultComparerWithSuccess")

Tests/UnitTests/Assert.NotInRange.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([=]() { Assert.NotInRange(0, 0, 1) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 FACT("NotInRangeNeedsValidRange")

Tests/UnitTests/Assert.NotNull.cpp

     static const std::string msg = "xUnit++";
     auto assert = Assert.Throws<xUnitAssert>([]() { Assert.NotNull(nullptr) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.NotSame.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.NotSame(obj, obj) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.Null.cpp

     auto x = std::make_shared<int>(0);
     auto assert = Assert.Throws<xUnitAssert>([&]() { Assert.Null(x) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.Same.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([=]() { Assert.Same(x, y) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/Assert.Throws.cpp

     }
     catch(const xUnitAssert &assert)
     {
-        Assert.Contains(assert.what(), userMessage.c_str());
+        Assert.Contains(assert.UserMessage(), userMessage.c_str());
         return;
     }
 

Tests/UnitTests/Assert.True.cpp

 
     auto assert = Assert.Throws<xUnitAssert>([]() { Assert.True(false) << msg; });
 
-    Assert.Contains(assert.what(), msg.c_str());
+    Assert.Contains(assert.UserMessage(), msg.c_str());
 }
 
 }

Tests/UnitTests/LineInfo.cpp

 SUITE("LineInfo")
 {
 
-FACT("LineInfoOverridesDefaultTestLineInfo")
+FACT("Passed-in LineInfo should override the test's LineInfo")
 {
     auto file = "filename";
     auto line = 1;

Tests/UnitTests/TestsCanOutputAnythingWithToString.cpp

 
 using HasToString::Point;
 
-FACT("EqualADL")
+FACT("Testing Equal with ADL")
 {
     Point p0(0, 0);
     Point p1(1, 1);
 
     auto result = Assert.Throws<xUnitpp::xUnitAssert>([&]() { Assert.Equal(p0, p1); });
 
-    Check.Contains(result.what(), "Expected: (0, 0)");
-    Check.Contains(result.what(), "Actual: (1, 1)");
+    Check.Contains(result.Expected(), "(0, 0)");
+    Check.Contains(result.Actual(), "(1, 1)");
 }
 
-FACT("RangeEqualADL")
+FACT("Testing range Equal with ADL")
 {
     Point p0[] = { Point(0, 0), Point(1, 0), Point(2, 0) };
     Point p1[] = { Point(0, 0), Point(1, 1), Point(2, 0) };
 
     auto result = Assert.Throws<xUnitpp::xUnitAssert>([&]() { Assert.Equal(&p0[0], &p0[3], &p1[0], &p1[3]); });
 
-    Check.Contains(result.what(), "Sequence unequal at location 1.");
-    Check.Contains(result.what(), "Expected: [ (0, 0), (1, 0), (2, 0) ]");
-    Check.Contains(result.what(), "Actual: [ (0, 0), (1, 1), (2, 0) ]");
+    Check.Contains(result.CustomMessage(), "Sequence unequal at location 1.");
+    Check.Contains(result.Expected(), "[ (0, 0), (1, 0), (2, 0) ]");
+    Check.Contains(result.Actual(), "[ (0, 0), (1, 1), (2, 0) ]");
 }
 
-FACT("InRangeADL")
+FACT("Testing InRange with ADL")
 {
     Point min(0, 0);
     Point max(10, 0);
 
     auto result = Assert.Throws<xUnitpp::xUnitAssert>([&]() { Assert.InRange(test, min, max); });
 
-    Check.Contains(result.what(), "Expected: [(0, 0) - (10, 0))");
-    Check.Contains(result.what(), "Actual: (20, 0)");
+    Check.Contains(result.Expected(), "[(0, 0) - (10, 0))");
+    Check.Contains(result.Actual(), "(20, 0)");
 }
 
-FACT("NotInRangeADL")
+FACT("Testing NotInRange with ADL")
 {
     Point min(0, 0);
     Point max(10, 0);
 
     auto result = Assert.Throws<xUnitpp::xUnitAssert>([&]() { Assert.NotInRange(test, min, max); });
 
-    Check.Contains(result.what(), "Expected: [(0, 0) - (10, 0))");
-    Check.Contains(result.what(), "Actual: (5, 0)");
+    Check.Contains(result.Expected(), "[(0, 0) - (10, 0))");
+    Check.Contains(result.Actual(), "(5, 0)");
 }
 
 }

xUnit++.console/CommandLine.cpp

 {
     Options::Options()
         : verbose(false)
-        , veryVerbose(false)
         , list(false)
         , timeLimit(0)
         , threadLimit(0)
                 {
                     options.verbose = true;
                 }
-                else if (opt == "-vv")
-                {
-                    options.verbose = true;
-                    options.veryVerbose = true;
-                }
                 else if (opt == "-l" || opt == "--list")
                 {
                     options.list = true;
             "\n"
             "options:\n\n"
             "  -v                             : Verbose mode: include successful test timing\n"
-            "  -vv                            : Very verbose: write test start message\n"
             "  -l --list                      : Do not run tests, just list the ones that pass the filters\n"
             "  -s --suite <SUITE>+            : Suite(s) of tests to run (substring match)\n"
             "  -n --name <TEST>+              : Test(s) to run (substring match)\n"

xUnit++.console/CommandLine.h

         Options();
 
         bool verbose;
-        bool veryVerbose;
         bool list;
         std::vector<std::string> suites;
         std::vector<std::string> testNames;

xUnit++.console/ConsoleReporter.cpp

 #include <iostream>
 #include <memory>
 #include <mutex>
+#include <unordered_map>
+
+#if defined (_WIN32)
+#include <Windows.h>
+#undef ReportEvent
+
+namespace
+{
+    class ResetConsoleColors
+    {
+    public:
+        ResetConsoleColors()
+            : stdOut(GetStdHandle(STD_OUTPUT_HANDLE))
+        {
+            CONSOLE_SCREEN_BUFFER_INFO info;
+            GetConsoleScreenBufferInfo(stdOut, &info);
+            attributes = info.wAttributes;
+        }
+
+        ~ResetConsoleColors()
+        {
+            Reset();
+        }
+
+        void Reset()
+        {
+            SetConsoleTextAttribute(stdOut, attributes);
+        }
+
+    private:
+        HANDLE stdOut;
+        WORD attributes;
+    } resetConsoleColors;
+}
+
+#else
+#endif
+
 #include "xUnit++/LineInfo.h"
 #include "xUnit++/TestDetails.h"
 #include "xUnit++/TestEvent.h"
 
-
 namespace
 {
+    static const std::string TestSeparator = "\n            ";
+
+    enum class Color : unsigned short
+    {
+        Default = 65535,
+
+        Black = 0,
+        DarkBlue,
+        DarkGreen,
+        DarkCyan,
+        DarkRed,
+        DarkMagenta,
+        DarkYellow,
+        LightGray,
+        DarkGray,
+        Blue,
+        Green,
+        Cyan,
+        Red,
+        Magenta,
+        Yellow,
+        White,
+
+        Debug = DarkMagenta,
+        Info = DarkCyan,
+        Warning = Yellow,
+        Check = Red,
+        Assert = Red,
+        Skip = Yellow,
+        Suite = White,
+        Separator = DarkGray,
+        TestName = White,
+        FileAndLine = Default,
+        Success = Green,
+        Failure = Red,
+        TimeSummary = DarkGray,
+        Call = White,
+        Expected = Cyan,
+        Actual = Cyan,
+
+        // this value is to match the Win32 value
+        // there is a special test in to_ansicode
+        Fatal = (0x40 + White),
+    };
+
+    Color to_color(xUnitpp::EventLevel level)
+    {
+        switch (level)
+        {
+        case xUnitpp::EventLevel::Debug:
+            return Color::Debug;
+        case xUnitpp::EventLevel::Info:
+            return Color::Info;
+        case xUnitpp::EventLevel::Warning:
+            return Color::Warning;
+        case xUnitpp::EventLevel::Check:
+            return Color::Check;
+        case xUnitpp::EventLevel::Assert:
+            return Color::Assert;
+        case xUnitpp::EventLevel::Fatal:
+            return Color::Fatal;
+        }
+
+        return Color::Default;
+    }
+
+    std::string to_ansicode(Color color)
+    {
+        static const std::string codes[] =
+        {
+            "\033[0;30m",
+            "\033[0;34m",
+            "\033[0;32m",
+            "\033[0;36m",
+            "\033[0;31m",
+            "\033[0;35m",
+            "\033[0;33m",
+            "\033[0;37m",
+            "\033[0;1;30m", // the 0 at the start clears the background color
+            "\033[0;1;34m", // then the 1 sets the color to bold
+            "\033[0;1;32m",
+            "\033[0;1;36m",
+            "\033[0;1;31m",
+            "\033[0;1;35m",
+            "\033[0;1;33m",
+            "\033[0;1;37m",
+        };
+
+        if (color == Color::Fatal)
+        {
+            return "\033[1;37;41m";
+        }
+
+        if (color != Color::Default)
+        {
+            return codes[(int)color];
+        }
+
+        return "\033[m";
+    }
+
     std::string FileAndLine(const xUnitpp::TestDetails &td, const xUnitpp::LineInfo &lineInfo)
     {
         auto result = to_string(lineInfo);
         return result;
     }
 
-    class CachedOutput
+    template<typename TStream>
+    TStream &operator <<(TStream &stream, Color color)
     {
-        typedef std::map<int, std::shared_ptr<CachedOutput>> OutputCache;
+#if defined (_WIN32)
+        stream.flush();
+        if (color != Color::Default)
+        {
+            SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), (unsigned short)color);
+        }
+        else
+        {
+            resetConsoleColors.Reset();
+        }
+#else
+        stream << to_ansicode(color);
+#endif
 
+        return stream;
+    }
+}
+
+namespace xUnitpp
+{
+
+class ConsoleReporter::ReportCache
+{
+    class TestOutput
+    {
     public:
-        CachedOutput(const std::string &name)
-            : name(name)
+        struct Fragment
+        {
+            Fragment(Color color, const std::string &message)
+                : color(color)
+                , message(message)
         {
         }
 
-        ~CachedOutput()
+            friend std::ostream &operator <<(std::ostream &stream, const Fragment &fragment)
         {
-            if (!messages.empty())
+                return stream << fragment.color << fragment.message;
+        }
+
+            Color color;
+            std::string message;
+        }; 
+
+        TestOutput(const xUnitpp::TestDetails &td, bool verbose)
+            : testDetails(td)
+            , failed(false)
+            , verbose(verbose)
+        {
+        }
+
+        ~TestOutput()
+        {
+            if (failed || verbose)
             {
-                std::cout << "\n[" << name << "]\n";
+                if (failed)
+                {
+                    std::cout << Fragment(Color::Failure, "\n[ Failure ] ");
+                }
+                else
+                {
+                    std::cout << Fragment(Color::Success, "\n[ Success ] ");
+                }
 
-                for (const auto &msg : messages)
+                if (!testDetails.Suite.empty())
                 {
-                    std::cout << msg << std::endl;
+                    std::cout << Fragment(Color::Suite, testDetails.Suite);
+                    std::cout << Fragment(Color::Separator, TestSeparator);
+                }
+
+                std::cout << Fragment(Color::TestName, testDetails.Name + "\n");
+            }
+
+            if (!fragments.empty())
+            {
+                for (auto &&msg : fragments)
+                {
+                    std::cout << msg;
+                }
+            } 
+        }
+
+        TestOutput &operator <<(const xUnitpp::TestEvent &event)
+        {
+            if (event.IsFailure())
+            {
+                failed = true;
+            }
+
+            fragments.emplace_back(Color::FileAndLine, FileAndLine(testDetails, event.LineInfo()));
+            fragments.emplace_back(Color::Separator, ": ");
+            fragments.emplace_back(to_color(event.Level()), to_string(event.Level()));
+            fragments.emplace_back(Color::Separator, ": ");
+
+            // unfortunately, this code should almost completely match the to_string(const TestEvent &)
+            // function found in TestEvent.cpp, but is kept separate because of the color coding
+            if (event.IsAssertType())
+            {
+                auto &&assert = event.Assert();
+
+                fragments.emplace_back(Color::Call, assert.Call() + "()");
+
+                std::string message = " failure";
+
+                std::string userMessage = assert.UserMessage();
+                if (!userMessage.empty())
+                {
+                    message += ": " + userMessage;
+
+                    if (!assert.CustomMessage().empty())
+                    {
+                        message += "\n     " + assert.CustomMessage();
+                    }
+                }
+                else if (!assert.CustomMessage().empty())
+                {
+                    message += ": " + assert.CustomMessage();
+                }
+                else
+                {
+                    message += ".";
+                }
+
+                fragments.emplace_back(Color::Default, message);
+
+                if (!assert.Expected().empty() || !assert.Actual().empty())
+                {
+                    fragments.emplace_back(Color::Expected, "\n     Expected: ");
+                    fragments.emplace_back(Color::Default, assert.Expected());
+                    fragments.emplace_back(Color::Actual, "\n       Actual: ");
+                    fragments.emplace_back(Color::Default, assert.Actual());
                 }
             }
+            else
+            {
+                fragments.emplace_back(Color::Default, event.Message());
+            }
+
+            fragments.emplace_back(Color::Default, "\n");
+            return *this;
         }
 
-        static void Instant(const std::string &output)
+        TestOutput &operator <<(xUnitpp::Time::Duration time)
         {
-            std::cout << output;
+            if (verbose)
+            {
+                fragments.emplace_back(Color::TimeSummary, "Test completed in " + xUnitpp::Time::to_string(time) + ".\n");
+            }
+
+            return *this;
         }
 
-        static CachedOutput &Cache(const xUnitpp::TestDetails &td)
-        {
-            auto &cache = Cache();
+    private:
+        TestOutput(const TestOutput &) /* = delete */;
+        TestOutput &operator =(TestOutput) /* = delete */;
 
+    private:
+        const xUnitpp::TestDetails &testDetails;
+        std::vector<Fragment> fragments;
+        bool failed;
+        bool verbose;
+    };
+
+    typedef std::unordered_map<int, std::shared_ptr<TestOutput>> OutputCache;
+    
+public:
+    ReportCache(bool verbose)
+        : verbose(verbose)
+    {
+    }
+
+    void Instant(Color color, const std::string &message)
+    {
+        std::cout << TestOutput::Fragment(color, message);
+    }
+
+    TestOutput &Cache(const xUnitpp::TestDetails &td)
+    {
             auto it = cache.find(td.Id);
             if (it == cache.end())
             {
-                cache.insert(std::make_pair(td.Id, std::make_shared<CachedOutput>(td.Name)));
+            cache.insert(std::make_pair(td.Id, std::make_shared<TestOutput>(td, verbose)));
             }
 
             return *cache[td.Id];
         }
 
-        static void Finish(const xUnitpp::TestDetails &td)
+    void Finish(const xUnitpp::TestDetails &td)
         {
-            auto &cache = Cache();
-
             auto it = cache.find(td.Id);
             if (it != cache.end())
             {
             }
         }
 
-        CachedOutput &operator <<(const std::string &output)
-        {
-            messages.push_back(output);
-            return *this;
-        }
+    private:
+    OutputCache cache;
+    bool verbose;
+    };
 
-    private:
-        static OutputCache &Cache()
-        {
-            static OutputCache cache;
-            return cache;
-        }
-
-        std::string name;
-        std::vector<std::string> messages;
-    };
-}
-
-namespace xUnitpp
-{
-
-ConsoleReporter::ConsoleReporter(bool verbose, bool veryVerbose)
-    : mVerbose(verbose)
-    , mVeryVerbose(veryVerbose)
+ConsoleReporter::ConsoleReporter(bool verbose)
+    : cache(new ReportCache(verbose))
 {
 }
 
-void ConsoleReporter::ReportStart(const TestDetails &testDetails)
+void ConsoleReporter::ReportStart(const TestDetails &)
 {
-    if (mVeryVerbose)
-    {
-        CachedOutput::Instant("Starting test " + testDetails.Name + ".");
-    }
 }
 
 void ConsoleReporter::ReportEvent(const TestDetails &testDetails, const TestEvent &evt)
 {
-    CachedOutput::Cache(testDetails) << (FileAndLine(testDetails, evt.LineInfo()) +
-        ": " + to_string(evt.Level()) + ": " + to_string(evt));
+    cache->Cache(testDetails) << evt;
 }
 
 void ConsoleReporter::ReportSkip(const TestDetails &testDetails, const std::string &reason)
 {
-    CachedOutput::Instant(to_string(testDetails.LineInfo) +
-        ": skipping " + testDetails.Name + ": " + reason);
+    cache->Instant(Color::Skip, "\n[ Skipped ] ");
+
+    if (!testDetails.Suite.empty())
+    {
+        cache->Instant(Color::Suite, testDetails.Suite);
+        cache->Instant(Color::Separator, TestSeparator);
+    }
+
+    cache->Instant(Color::TestName, testDetails.ShortName + "\n");
+    cache->Instant(Color::FileAndLine, to_string(testDetails.LineInfo) + ": ");
+    cache->Instant(Color::Skip, reason);
+    cache->Instant(Color::Default, "\n");
 }
 
 void ConsoleReporter::ReportFinish(const TestDetails &testDetails, Time::Duration timeTaken)
 {
-    if (mVerbose)
-    {
-        auto ms = Time::ToMilliseconds(timeTaken);
-        CachedOutput::Cache(testDetails) << (testDetails.Name + ": Completed in " +
-            (ms.count() == 0 ? (std::to_string(timeTaken.count()) + " nanoseconds.\n") : (std::to_string(ms.count()) + " milliseconds.\n")));
-    }
-
-    CachedOutput::Finish(testDetails);
+    cache->Cache(testDetails) << timeTaken;
+    cache->Finish(testDetails);
 }
 
 void ConsoleReporter::ReportAllTestsComplete(size_t testCount, size_t skipped, size_t failureCount, Time::Duration totalTime)
 {
-    std::string total = std::to_string(testCount) + " tests, ";
-    std::string failures = std::to_string(failureCount) + " failed, ";
-    std::string skips = std::to_string(skipped) + " skipped.";
-
-    std::string header;
-
+    Color failColor = Color::Default;
+    Color skipColor = Color::Default;
     if (failureCount > 0)
     {
-        header = "\nFAILURE: ";
+        failColor = Color::Failure;
+        cache->Instant(failColor, "\nFAILURE");
     }
     else if (skipped > 0)
     {
-        header = "\nWARNING: ";
+        skipColor = Color::Warning;
+        cache->Instant(skipColor, "\nWARNING");
     }
     else
     {
-        header = "\nSuccess: ";
+        cache->Instant(Color::Success, "\nSuccess");
     }
 
-    CachedOutput::Instant(header + total + failures + skips);
+    cache->Instant(Color::Default, ": " + std::to_string(testCount) + " tests, ");
+    cache->Instant(failColor, std::to_string(failureCount) + " failed");
+    cache->Instant(Color::Default, ", ");
+    cache->Instant(skipColor, std::to_string(skipped) + " skipped");
+    cache->Instant(Color::Default, ".");
 
-    header = "\nTest time: ";
+    std::string report = "\nTest time: ";
 
     auto ms = Time::ToMilliseconds(totalTime);
 
     if (ms.count() > 500)
     {
-        CachedOutput::Instant(header + std::to_string(Time::ToSeconds(totalTime).count()) + " seconds.");
+        report += std::to_string(Time::ToSeconds(totalTime).count()) + " seconds.";
     }
     else
     {
-        CachedOutput::Instant(header + std::to_string(ms.count()) + " milliseconds.");
+        report += std::to_string(ms.count()) + " milliseconds.";
     }
+
+    cache->Instant(Color::TimeSummary, report);
+    cache->Instant(Color::Default, "\n");
 }
 
 }

xUnit++.console/ConsoleReporter.h

 #ifndef CONSOLEREPORTER_H_
 #define CONSOLEREPORTER_H_
 
+#include <memory>
 #include "xUnit++/IOutput.h"
 
 namespace xUnitpp
 class ConsoleReporter : public IOutput
 {
 public:
-    ConsoleReporter(bool verbose, bool veryVerbose);
+    ConsoleReporter(bool verbose);
 
     virtual void ReportStart(const TestDetails &) override;
     virtual void ReportEvent(const TestDetails &testDetails, const TestEvent &evt) override;
     virtual void ReportAllTestsComplete(size_t testCount, size_t skipped, size_t failureCount, Time::Duration totalTime) override;
 
 private:
-    bool mVerbose;
-    bool mVeryVerbose;
+    class ReportCache;
+    std::unique_ptr<ReportCache> cache;
 };
 
 }

xUnit++.console/main.cpp

             std::sort(activeTestIds.begin(), activeTestIds.end());
 
             std::unique_ptr<xUnitpp::IOutput> reporter(options.xmlOutput.empty() ?
-                (xUnitpp::IOutput *)new xUnitpp::ConsoleReporter(options.verbose, options.veryVerbose) :
+                (xUnitpp::IOutput *)new xUnitpp::ConsoleReporter(options.verbose) :
                 (xUnitpp::IOutput *)new xUnitpp::Utilities::XmlReporter(options.xmlOutput));
             totalFailures += testAssembly.FilteredTestsRunner(options.timeLimit, options.threadLimit, *reporter,
                 [&](const xUnitpp::TestDetails &testDetails)

xUnit++/src/TestEvent.cpp

 {
     static std::string msg[] =
     {
-        "DEBUG",
-        "info",
-        "warning",
-        "error",
-        "error",
-        "error"
+        "[   DEBUG ]",
+        "[    Info ]",      
+        "[ Warning ]",
+        "[   Check ]",
+        "[  Assert ]",
+        "[   Fatal ]"
     };
 
     return msg[(int)level];
 }
 
-const std::string &to_string(const TestEvent &evt)
+std::string to_string(const TestEvent &event)
 {
-    return evt.message;
+    if (event.IsAssertType())
+    {
+        auto &&assert = event.Assert();
+        auto message = assert.Call() + "() failure";
+
+        std::string userMessage = assert.UserMessage();
+        if (!userMessage.empty())
+        {
+            message += ": " + userMessage;
+
+            if (!assert.CustomMessage().empty())
+            {
+                message += "\n     " + assert.CustomMessage();
+            }
+        }
+        else if (!assert.CustomMessage().empty())
+        {
+            message += ": " + assert.CustomMessage();
+        }
+        else
+        {
+            message += ".";
+        }
+
+        if (!assert.Expected().empty() || !assert.Actual().empty())
+        {
+            message += "\n     Expected: ";
+            message += assert.Expected();
+            message += "\n       Actual: ";
+            message += assert.Actual();
+        }
+
+        return message;
+    }
+
+    return event.message;
 }
 
 TestEvent::TestEvent(EventLevel level, const std::string &message, const xUnitpp::LineInfo &lineInfo)
     : level(level)
+    , assert(xUnitAssert::None())
     , message(message)
     , lineInfo(lineInfo)
 {
 
 TestEvent::TestEvent(EventLevel level, const xUnitAssert &assert)
     : level(level)
-    , message(assert.what())
+    , assert(assert)
     , lineInfo(assert.LineInfo())
 {
 }
 
 TestEvent::TestEvent(const std::exception &e)
     : level(EventLevel::Fatal)
+    , assert(xUnitAssert::None())
     , message(e.what())
 {
 }
     return level;
 }
 
+bool TestEvent::IsAssertType() const
+{
+    return !assert.Call().empty();
+}
+
+const xUnitpp::xUnitAssert &TestEvent::Assert() const
+{
+    return assert;
+}
+
+const std::string &TestEvent::Message() const
+{
+    return message;
+}
+
 const xUnitpp::LineInfo &TestEvent::LineInfo() const
 {
     return lineInfo;

xUnit++/src/xUnitAssert.cpp

 #include "xUnitAssert.h"
 #include <cmath>
 
-namespace
-{
-    std::string AssembleWhat(const std::string &call, const std::vector<std::string> &userMsg, const std::string &customMsg,
-                             const std::string &expected, const std::string &actual)
-    {
-        std::string msg = call + "() failure";
-        if (!userMsg.empty())
-        {
-            msg += ": ";
-
-            for (const auto &s : userMsg)
-            {
-                msg += s;
-            }
-
-            if (!customMsg.empty())
-            {
-                msg += "\n     " + customMsg;
-            }
-        }
-        else
-        {
-            if (!customMsg.empty())
-            {
-                msg += ": " + customMsg;
-            }
-        }
-
-        if (!expected.empty())
-        {
-            msg += "\n     Expected: " + expected;
-            msg += "\n       Actual: " + actual;
-        }
-
-        return msg;
-    }
-}
-
 namespace xUnitpp
 {
 
 xUnitAssert::xUnitAssert(std::string &&call, xUnitpp::LineInfo &&lineInfo)
     : lineInfo(std::move(lineInfo))
     , call(std::move(call))
+    , userMessage(std::make_shared<std::stringstream>())
 {
 }
 
     return *this;
 }
 
+const std::string &xUnitAssert::Call() const
+{
+    return call;
+}
+
+std::string xUnitAssert::UserMessage() const
+{
+    return userMessage->str();
+}
+
+const std::string &xUnitAssert::CustomMessage() const
+{
+    return customMessage;
+}
+
+const std::string &xUnitAssert::Expected() const
+{
+    return expected;
+}
+
+const std::string &xUnitAssert::Actual() const
+{
+    return actual;
+}
+
 const LineInfo &xUnitAssert::LineInfo() const
 {
     return lineInfo;
 }
 
-const char *xUnitAssert::what() const noexcept(true)
-{
-    if (whatMessage.empty())
-    {
-        whatMessage = AssembleWhat(call, userMessage, customMessage, expected, actual);
-    }
-
-    return whatMessage.c_str();
-}
-
 xUnitFailure::xUnitFailure()
     : OnFailureComplete([](const xUnitAssert &){})
     , assert(xUnitAssert::None())
     if (actualString.find(value) == std::string::npos)
     {
         return OnFailure(std::move(xUnitAssert(callPrefix + "Contains", std::move(lineInfo))
-            .Expected(std::string(actualString))    // can't assume actualString or value can be moved
-            .Actual(std::string(value))));
+            .Expected(std::string(value))    // can't assume actualString or value can be moved
+            .Actual(std::string(actualString))));
     }
 
     return OnSuccess();

xUnit++/xUnit++/Suite.h

 {
     inline const std::string &Name()
     {
-        static std::string name = "xUnit++ Default Suite";
+        static std::string name = "";
         return name;
     }
 

xUnit++/xUnit++/TestEvent.h

 #include <exception>
 #include <string>
 #include "LineInfo.h"
+#include "xUnitAssert.h"
 
 namespace xUnitpp
 {
 
-class xUnitAssert;
-
 enum class EventLevel
 {
     Debug,
     TestEvent(const std::exception &e);
 
     bool IsFailure() const;
+    bool IsAssertType() const;
+
+    const xUnitAssert &Assert() const;
+    const std::string &Message() const;
 
     EventLevel Level() const;
     const xUnitpp::LineInfo &LineInfo() const;
 
-    friend const std::string &to_string(const TestEvent &evt);
+    friend std::string to_string(const TestEvent &event);
 
 private:
     EventLevel level;
+    xUnitAssert assert;
     std::string message;
     xUnitpp::LineInfo lineInfo;
 };

xUnit++/xUnit++/xUnitAssert.h

 #include <algorithm>
 #include <exception>
 #include <functional>
+#include <memory>
 #include <sstream>
 #include <stdexcept>
 #include <string>
 namespace xUnitpp
 {
 
-class xUnitAssert : public std::exception
+class xUnitAssert
 {
-    typedef std::exception base;
-
 public:
     xUnitAssert(std::string &&call, LineInfo &&lineInfo);
 
     template<typename T>
     xUnitAssert &AppendUserMessage(T &&value)
     {
-        std::stringstream str;
-        str << std::forward<T>(value);
-        userMessage.push_back(str.str());
-
+        *userMessage << value;
         return *this;
     }
 
+    const std::string &Call() const;
+    std::string UserMessage() const;
+    const std::string &CustomMessage() const;
+    const std::string &Expected() const;
+    const std::string &Actual() const;
+
     const xUnitpp::LineInfo &LineInfo() const;
 
     static const xUnitAssert &None();
 
-    virtual const char *what() const noexcept(true) override;
-
 private:
     xUnitpp::LineInfo lineInfo;
     std::string call;
     std::string customMessage;
     std::string expected;
     std::string actual;
-    std::vector<std::string> userMessage;
-
-    mutable std::string whatMessage;
+    std::shared_ptr<std::stringstream> userMessage;
 };
 
 class xUnitFailure

xUnit++/xUnit++/xUnitTime.h

     return Duration(ns);
 }
 
+inline std::string to_string(Duration time)
+{
+    using std::to_string;
+
+    auto ms = ToMilliseconds(time);
+
+    if (ms.count() < 1)
+    {
+        return to_string(time.count()) + " nanoseconds";
+    }
+
+    if (ms.count() > 500)
+    {
+        return to_string(ToSeconds(time).count()) + " seconds";
+    }
+
+    return to_string(ms.count()) + " milliseconds";
+}
+
 }}
 
 #endif