From 4a13fbefe61a93d0f96b068805378e8406617518 Mon Sep 17 00:00:00 2001
From: Imdad Sardharwalla <imdad.sardharwalla@autodesk.com>
Date: Wed, 4 Aug 2021 11:11:20 +0100
Subject: [PATCH] REFORM-1144 Restructure checksum comparison code (#59)

* Create a new Compare class in TestChecksumCompare.(cc|hh) that
  contains the code from the function Test::compare_checksums()
  (previously in ReForm Preprocess) and Test::compare_reports()
  (previously in TestResultAnalysis.(cc|hh)).

  Checksum objects reside in the binary that implements the tested
  algorithms (A), which may not be the same binary that runs the
  comparisons (B). In order to deal with this, we create an instance
  of the Compare class for each copy of Base contained within the
  final binary. The Compare object from A can be provided to B to
  perform the comparisons.

* Create NullOutputStream and a global instance Base::null_os. This
  ignores any data streamed to it.

Co-authored-by: Martin Marinov <martin.marinov@autodesk.com>
---
 Test/CMakeLists.txt                           |   3 +-
 Test/TestCase.hh                              |  28 +-
 ...sultAnalysis.cc => TestChecksumCompare.cc} | 245 +++++++++++++++---
 Test/TestChecksumCompare.hh                   |  85 ++++--
 Test/TestReport.cc                            |  28 +-
 Test/TestReport.hh                            |   5 +-
 Test/TestResultAnalysis.hh                    |  42 ---
 Utils/CMakeLists.txt                          |   2 +
 Utils/NullOutputStream.cc                     |  11 +
 Utils/NullOutputStream.hh                     |  27 ++
 10 files changed, 347 insertions(+), 129 deletions(-)
 rename Test/{TestResultAnalysis.cc => TestChecksumCompare.cc} (51%)
 mode change 100644 => 100755
 delete mode 100644 Test/TestResultAnalysis.hh
 create mode 100755 Utils/NullOutputStream.cc
 create mode 100755 Utils/NullOutputStream.hh

diff --git a/Test/CMakeLists.txt b/Test/CMakeLists.txt
index fa9d180..11f42ad 100644
--- a/Test/CMakeLists.txt
+++ b/Test/CMakeLists.txt
@@ -16,7 +16,6 @@ set(my_headers
    ${CMAKE_CURRENT_SOURCE_DIR}/TestPaths.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/TestReport.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/TestResult.hh
-   ${CMAKE_CURRENT_SOURCE_DIR}/TestResultAnalysis.hh
    PARENT_SCOPE
 )
 
@@ -24,6 +23,7 @@ set(my_headers
 set(my_sources
    ${CMAKE_CURRENT_SOURCE_DIR}/TestArgs.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksum.cc
+   ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumCompare.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumCompletion.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumCondition.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestChecksumDebugEvent.cc
@@ -33,6 +33,5 @@ set(my_sources
    ${CMAKE_CURRENT_SOURCE_DIR}/TestOutcome.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestPaths.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/TestReport.cc
-   ${CMAKE_CURRENT_SOURCE_DIR}/TestResultAnalysis.cc
    PARENT_SCOPE
 )
diff --git a/Test/TestCase.hh b/Test/TestCase.hh
index 6a1dea6..473caaa 100755
--- a/Test/TestCase.hh
+++ b/Test/TestCase.hh
@@ -5,26 +5,21 @@
 
 #ifdef TEST_ON
 
+#include <Base/Test/TestChecksumCompare.hh>
+
 /*
 The BASE_TEST() macro wraps around an existing test declaration macro (e.g.
 TEST_CASE() for Catch2), augmenting it with a checksum comparison. For now, the
 existing test declaration macro must define a void function.
 
-In order to use BASE_TEST(), the following macros *must* be defined (before this
+In order to use BASE_TEST(), the following macro *must* be defined (before this
 file is included):
 
-- TEST_DECLARATION: a placeholder for the existing test declaration macro;
-
-- TEST_COMPARE_CHECKSUMS: a placeholder for a function that compares checksums
-  output by the currently-running test against the corresponding baseline.
+  TEST_DECLARATION: a placeholder for the existing test declaration macro;
 
 As an example, to use this with Catch2, one would write:
 
-    #define TEST_DECLARATION TEST_CASE
-
-And to compare the checksums using Test::compare_checksums(), one would write:
-
-    #define TEST_COMPARE_CHECKSUMS Test::compare_checksums()
+  #define TEST_DECLARATION TEST_CASE
 
 It is not required to use BASE_TEST(), and it is straightforward to define a
 custom macro with a similar function. The following macros are available for
@@ -51,30 +46,29 @@ use:
 
 // Both TEST_DECLARATION and TEST_COMPARE_CHECKSUMS must be defined in order to
 // use the BASE_TEST() macro.
-#if defined(TEST_DECLARATION) && defined(TEST_COMPARE_CHECKSUMS)
+#ifdef TEST_DECLARATION
 
 #define INTERNAL_BASE_TEST_VOID(TEST_FUNCTION, ...) \
   static void TEST_FUNCTION(); \
   TEST_DECLARATION(__VA_ARGS__) \
   { \
     TEST_FUNCTION(); \
-    TEST_COMPARE_CHECKSUMS; \
+    ::Test::Checksum::compare.check_equal(); \
   } \
   void TEST_FUNCTION()
 
 #define BASE_TEST(...) \
   INTERNAL_BASE_TEST_VOID(INTERNAL_UNIQUE_BASE_TEST_FUNCTION_NAME, __VA_ARGS__)
 
-#else // defined(TEST_DECLARATION) && defined(TEST_COMPARE_CHECKSUMS)
+#else // TEST_DECLARATION
 
 #define BASE_TEST(...) \
   static_assert(false, \
-      "Both TEST_DECLARATION and TEST_COMPARE_CHECKSUMS must be " \
-      "defined in order to use the BASE_TEST() macro. Please ensure " \
-      "they are defined before TestCase.hh is included."); \
+      "TEST_DECLARATION must be defined in order to use the BASE_TEST() " \
+      "macro. Please ensure it is defined before TestCase.hh is included."); \
   static void INTERNAL_UNIQUE_BASE_TEST_FUNCTION_NAME()
 
-#endif // defined(TEST_DECLARATION) && defined(TEST_COMPARE_CHECKSUMS)
+#endif // TEST_DECLARATION
 
 #endif // TEST_ON
 #endif // BASE_TESTCASE_HH_INCLUDED
diff --git a/Test/TestResultAnalysis.cc b/Test/TestChecksumCompare.cc
old mode 100644
new mode 100755
similarity index 51%
rename from Test/TestResultAnalysis.cc
rename to Test/TestChecksumCompare.cc
index bb3abeb..732cd8d
--- a/Test/TestResultAnalysis.cc
+++ b/Test/TestChecksumCompare.cc
@@ -3,22 +3,28 @@
 #ifdef TEST_ON
 
 #include <Base/Security/Mandatory.hh>
-#include <Base/Paths/Filesystem.hh>
 
-#include "TestResultAnalysis.hh"
-#include "LongestCommonSubsequenceT.hh"
-#include "TestChecksum.hh"
+#include "TestChecksumCompare.hh"
 #include "TestChecksumCompletion.hh"
-#include "TestChecksumFile.hh"
 
-#include <algorithm>
-#include <cctype>
+#include "LongestCommonSubsequenceT.hh"
+
+#include <Base/Paths/Filesystem.hh>
+#include <Base/Utils/NullOutputStream.hh>
+#include <Base/Paths/PathLink.hh>
+
+#include <exception>
 #include <fstream>
 #include <iostream>
 #include <sstream>
+#include <string>
 
 namespace Test
 {
+using PathLink = Base::PathLink;
+
+namespace Checksum
+{
 
 namespace
 {
@@ -33,8 +39,8 @@ void erase_code_links(std::string& _str)
   const char* const CODE_LINK_END = "]";
 
   for (auto bgn_pstn = _str.find(CODE_LINK_BEGIN);
-        bgn_pstn != std::string::npos && !_str.empty();
-        bgn_pstn = _str.find(CODE_LINK_BEGIN, bgn_pstn))
+       bgn_pstn != std::string::npos && !_str.empty();
+       bgn_pstn = _str.find(CODE_LINK_BEGIN, bgn_pstn))
   {
     const auto end_pstn = _str.find(CODE_LINK_END, bgn_pstn);
     if (end_pstn == std::string::npos)
@@ -52,7 +58,8 @@ inline void rtrim(std::string& s)
       s.end());
 }
 
-// Read and parse lines of a report to get the checksum Result, name and content.
+// Read and parse lines of a report to get the checksum Result, name and
+// content.
 class Report
 {
 public:
@@ -61,8 +68,7 @@ public:
   {
   public:
     Entry() {}
-    explicit Entry(const std::string& _line)
-      : line_(_line)
+    explicit Entry(const std::string& _line) : line_(_line)
     {
       // Ignore lines that represent (debug) callstack groups
       if (group())
@@ -106,8 +112,8 @@ public:
     const Checksum::Record& record() const { return rcrd_; }
 
   private:
-    std::string line_; // Current line
-    std::string name_; // Extracted checksum name
+    std::string line_;      // Current line
+    std::string name_;      // Extracted checksum name
     Checksum::Record rcrd_; // Extracted checksum record
 
   private:
@@ -156,7 +162,7 @@ public:
 
   size_t size() const { return entrs_.size(); }
 
-  const Entry& operator[] (const size_t _i) const { return entrs_[_i]; }
+  const Entry& operator[](const size_t _i) const { return entrs_[_i]; }
 
 private:
   Checksum::Level lvl_;
@@ -165,9 +171,49 @@ private:
 
 } // namespace
 
-DifferenceDistribution compare_reports(const fs::path& _rprt_path0,
-    const fs::path& _rprt_path1, Base::IOutputStream& _log,
-    const Checksum::Compare& _chks_cmpr, const bool _shrt_frmt)
+// "Hidden" import of this function from TestChecksum.cc
+Difference compare_from_registry(
+    const String& _name, const Record& _old_rcrd, const Record& _new_rcrd);
+
+class Compare::Impl
+{
+public:
+  //! Set the report pair to compare
+  void set_reports(const fs::path& _left_path, const fs::path& _right_path);
+
+  //! Set the short format, i.e., remove identical checksums, true by default.
+  void set_short_format(const bool _short_format = true);
+
+  /*!
+  Run the comparison and generate a difference report. Throws a std error if the
+  report paths are not set.
+  */
+  DifferenceDistribution run(Base::IOutputStream& _log_os) const;
+
+  /*!
+  Run the comparison and throw a std error if both reports exist but the
+  checksums are not equal.
+  */
+  void check_equal() const;
+
+private:
+  fs::path left_path_;
+  fs::path right_path_;
+  bool short_format_ = true;
+};
+
+void Compare::Impl::set_reports(const fs::path& _left_path, const fs::path& _right_path)
+{
+  left_path_ = _left_path;
+  right_path_ = _right_path;
+}
+
+void Compare::Impl::set_short_format(const bool _short_format)
+{
+  short_format_ = _short_format;
+}
+
+DifferenceDistribution Compare::Impl::run(Base::IOutputStream& _log_os) const
 {
   using Base::ENDL;
 
@@ -182,7 +228,15 @@ DifferenceDistribution compare_reports(const fs::path& _rprt_path0,
   const char* const DIFF_TAG    = "!   ";
   const char* const RESULT_TAG  = "  ";
 
-  const Report rprts[2] = {Report(_rprt_path0), Report(_rprt_path1)};
+  // Throw an error if the report paths have not been set
+  if (left_path_.empty() || right_path_.empty())
+  {
+    throw std::runtime_error(
+        "Unable to compare checksum reports: Report paths have not been set. "
+        "This should be done using Test::Checksum::Compare::set_reports().");
+  }
+
+  const Report rprts[2] = {Report(left_path_), Report(right_path_)};
 
   LongestCommonSubsequenceT<Report> rprt_lcs(rprts[0], rprts[1]);
   rprt_lcs.trace();
@@ -200,32 +254,32 @@ DifferenceDistribution compare_reports(const fs::path& _rprt_path0,
 
       if (old_entr.group()) // this match entry is a group?
       {
-        if (!_shrt_frmt)
-          _log << GROUP_TAG << old_entr.line() << ENDL; // just print it
+        if (!short_format_)
+          _log_os << GROUP_TAG << old_entr.line() << ENDL; // just print it
       }
       else // checksum case
       {
-        const auto diff =
-            _chks_cmpr(old_entr.name(), old_entr.record(), new_entr.record());
+        const auto diff = compare_from_registry(
+            old_entr.name(), old_entr.record(), new_entr.record());
         if (diff.equal()) // no difference?
         {
-          if (!_shrt_frmt)
-            _log << SAME_TAG << old_entr.line() << ENDL; // just print it
+          if (!short_format_)
+            _log_os << SAME_TAG << old_entr.line() << ENDL; // just print it
         }
         else
         { // print difference
-          _log << NAME_TAG << old_entr.name() << ": " << diff.type_text()
-               << ENDL;
-          _log << OLD_TAG << old_entr.record().rslt << RESULT_TAG
-               << old_entr.record().data << ENDL;
-          _log << NEW_TAG << new_entr.record().rslt << RESULT_TAG
-               << new_entr.record().data << ENDL;
+          _log_os << NAME_TAG << old_entr.name() << ": " << diff.type_text()
+                  << ENDL;
+          _log_os << OLD_TAG << old_entr.record().rslt << RESULT_TAG
+                  << old_entr.record().data << ENDL;
+          _log_os << NEW_TAG << new_entr.record().rslt << RESULT_TAG
+                  << new_entr.record().data << ENDL;
           const char rslt_mark =
               old_entr.record().rslt.type() == new_entr.record().rslt.type()
                   ? ' '
                   : '!';
-          _log << DIFF_TAG << rslt_mark << RESULT_TAG << diff.description()
-               << ENDL;
+          _log_os << DIFF_TAG << rslt_mark << RESULT_TAG << diff.description()
+                  << ENDL;
         }
         ++test_diff[diff];
       }
@@ -233,7 +287,7 @@ DifferenceDistribution compare_reports(const fs::path& _rprt_path0,
     else if (ij.i_valid() && rprts[0].level() <= rprts[1].level())
     { // old entry removed and the old checksum level is equal or stricter
       const auto& old_entr = rprts[0][ij.i];
-      _log << REMOVED_TAG << old_entr.line() << ENDL; // just print it
+      _log_os << REMOVED_TAG << old_entr.line() << ENDL; // just print it
       if (!old_entr.group()) // if it is a group change, just ignore it
       {
         if (Checksum::completion.end(old_entr.line()))
@@ -245,7 +299,7 @@ DifferenceDistribution compare_reports(const fs::path& _rprt_path0,
     else if (ij.j_valid() && rprts[0].level() >= rprts[1].level())
     { // new entry added and the new checksum level is stricter or equal
       const auto& new_entr = rprts[1][ij.j];
-      _log << ADDED_TAG << new_entr.line() << ENDL; // just print it
+      _log_os << ADDED_TAG << new_entr.line() << ENDL; // just print it
       if (!new_entr.group()) // if it is a group change, just ignore it
       {
         if (Checksum::completion.end(new_entr.line()))
@@ -259,6 +313,127 @@ DifferenceDistribution compare_reports(const fs::path& _rprt_path0,
   return test_diff;
 }
 
+void Compare::Impl::check_equal() const
+{
+  std::cout << "Info: Comparing checksums...";
+
+  // Throw an error if the report paths have not been set
+  if (left_path_.empty() || right_path_.empty())
+  {
+    throw std::runtime_error(
+        "Unable to compare checksum reports: Report paths have not been set. "
+        "This should be done using Test::Checksum::Compare::set_reports().");
+  }
+
+  // Skip the comparison if one of these files doesn't exist
+  if (!fs::exists(left_path_))
+  {
+    std::cout << "no baseline report found (expected at "
+              << left_path_.generic_string() << ")." << std::endl;
+    return;
+  }
+
+  if (!fs::exists(right_path_))
+  {
+    std::cout << "no checksum report found for the current test (expected at "
+              << right_path_.generic_string() << ")." << std::endl;
+    return;
+  }
+
+  // Compare the test report against the baseline report file
+  Base::OStringStream log_os;
+  auto diffs = run(log_os);
+
+  std::cout << "complete. ";
+
+  if (diffs.empty())
+  {
+    // No differences found
+    std::cout << "No differences found." << std::endl;
+  }
+  else
+  {
+    // Differences found: display short diff and throw error
+    std::cout << "Differences found!" << std::endl << std::endl;
+    std::cout << log_os.str << std::endl;
+
+    std::cout << "Compare " << PathLink(left_path_) << " with "
+              << PathLink(right_path_) << "." << std::endl;
+    throw std::runtime_error("Checksum comparison failed--differences found!");
+  }
+}
+
+Compare::Impl* Compare::impl_ = nullptr;
+bool Compare::owned_ = false;
+
+void Compare::make()
+{
+  if (impl_ != nullptr)
+    return;
+  impl_ = new Impl;
+  owned_ = true;
+}
+
+void Compare::set(Compare& _othr)
+{
+  if (_othr.impl_ != nullptr)
+  {
+    throw std::runtime_error(
+        "Unable to set implementation: incoming implementation is null.");
+  }
+
+  if (impl_ != nullptr)
+  {
+    throw std::runtime_error(
+        "Unable to set implementation: implementation has already been set.");
+  }
+
+  impl_ = _othr.impl_;
+}
+
+Compare::~Compare()
+{
+  if (owned_)
+    delete impl_;
+}
+
+void Compare::set_reports(
+    const fs::path& _left_path, const fs::path& _right_path)
+{
+  check_implementation();
+  impl_->set_reports(_left_path, _right_path);
+}
+
+void Compare::set_short_format(const bool _short_format)
+{
+  check_implementation();
+  impl_->set_short_format(_short_format);
+}
+
+DifferenceDistribution Compare::run(Base::IOutputStream& _log_os) const
+{
+  check_implementation();
+  return impl_->run(_log_os);
+}
+
+void Compare::check_equal() const
+{
+  check_implementation();
+  impl_->check_equal();
+}
+
+void Compare::check_implementation() const
+{
+  if (impl_ == nullptr)
+  {
+    throw std::runtime_error("Implementation is null: Implementation should be "
+                             "set using Test::Checksum::Compare::make() or "
+                             "Test::Checksum::Compare::set().");
+  }
+}
+
+Compare compare;
+} // namespace Checksum
 } // namespace Test
 
 #endif // TEST_ON
diff --git a/Test/TestChecksumCompare.hh b/Test/TestChecksumCompare.hh
index bc0c2f0..9c627df 100644
--- a/Test/TestChecksumCompare.hh
+++ b/Test/TestChecksumCompare.hh
@@ -6,31 +6,84 @@
 #ifdef TEST_ON
 
 #include <Base/Test/TestChecksum.hh>
+#include <Base/Paths/Filesystem.hh>
+#include <Base/Utils/NullOutputStream.hh>
 
-#include <functional>
+#include <map>
 
 namespace Test
 {
+namespace fs = Base::filesystem;
+
 namespace Checksum
 {
+
+typedef std::map<Difference, size_t> DifferenceDistribution;
+
 /*!
-Function type to compare two records of the same checksum identified by name.
-The compare function should return \ref Difference value to describe the
-difference of the old vs the new value.
-*/
-typedef std::function<Difference(const String& /*!<[in] checksum name */,
-    const Record& /*!<[in] checksum old value */,
-    const Record& /*!<[in] checksum new value */)>
-    Compare;
-
-/*! 
-Compare function that uses the compare method of registered checksums.
-\note Make sure to wrap this appropriately for shared object export when using
-in shared binary components in order to use the registry in the shared binary!
+Compare two test checksum reports.
+
+This class should be instantiated uniquely in a specific way to ensure it is
+shared across different instances of Base in separate binary modules in the same
+process.
+
+As the checksums reside in the binary that implements the tested algorithms, the
+class instance should be created there with \ref make(). The instance is owned
+by that binary and must be deleted by it, as it's stored on its heap.
+
+Once created, the instance should be \ref set() in the test binary (usually an
+executable). \ref set() will not take ownership of the pointer. With this
+approach all class methods will return the same instance throughout the process.
+
+\note These considerations are irrelevant if the tested and test executable are
+linked statically, i.e., they share the same binary.
 */
-Difference compare_from_registry(
-    const String& _name, const Record& _old_rcrd, const Record& _new_rcrd);
+class Compare
+{
+public:
+  //! Make and store the unique instance
+  static void make();
+
+  //! Set an instance made in another binary
+  static void set(Compare&);
 
+public:
+  //! Destroy the owned instance
+  ~Compare();
+
+  //! Set the report pair to compare
+  void set_reports(const fs::path& _left_path, const fs::path& _right_path);
+
+  //! Set the short format, i.e., remove identical checksums, true by default.
+  void set_short_format(const bool _short_format = true);
+
+  /*!
+  Run the comparison and generate a difference report. Throws a std error if the
+  report paths are not set.
+  */
+  DifferenceDistribution run(
+      Base::IOutputStream& _log_os = Base::null_os) const;
+
+  /*!
+  Run the comparison and throw a std error if both reports exist but the
+  checksums are not equal.
+  */
+  void check_equal() const;
+
+private:
+  //! Throw std error if impl_ is nullptr
+  void check_implementation() const;
+
+private:
+  class Impl;
+  static Impl* impl_; // shared implementation pointer
+  static bool owned_; // own implementation or not
+};
+
+/*!
+Shared instance per binary, make sure to call make() and set() as appropriate.
+*/
+extern Compare compare;
 } // namespace Checksum
 } // namespace Test
 
diff --git a/Test/TestReport.cc b/Test/TestReport.cc
index 74738d0..df9cb04 100644
--- a/Test/TestReport.cc
+++ b/Test/TestReport.cc
@@ -6,10 +6,10 @@
 #include <Base/Paths/Filesystem.hh>
 #include <Base/Paths/PathLink.hh>
 #include "TestChecksum.hh"
+#include "TestChecksumCompare.hh"
 #include "TestChecksumCompletion.hh"
 #include "TestReport.hh"
 #include "TestResult.hh"
-#include "TestResultAnalysis.hh"
 
 #include <cstring>
 #include <fstream>
@@ -236,7 +236,8 @@ typedef std::stringstream TestStream;
 struct TestDiffSummary
 {
   TestDiffSummary(const fs::path& _test_path,
-      const Test::DifferenceDistribution& _diff, const TestStream& _descr)
+      const Test::Checksum::DifferenceDistribution& _diff,
+      const TestStream& _descr)
       : path_(_test_path), diffs_(_diff), descr_(std::move(_descr.str()))
   {
   }
@@ -274,7 +275,7 @@ struct TestDiffSummary
   bool equivalent() const { return diffs_.empty(); }
 
   fs::path path_;
-  Test::DifferenceDistribution diffs_;
+  Test::Checksum::DifferenceDistribution diffs_;
   std::string descr_; // Test report of test differences description.
 };
 
@@ -457,7 +458,7 @@ void replace(const fs::path& _root_source, const fs::path& _root_target,
 // right.
 ExitStatus make_comparison(const char* const _dir_left,
     const char* const _dir_right, const CompareOutputType _cot,
-    const Checksum::Compare& _chks_cmpr, const bool _show_progress)
+    const bool _show_progress)
 {
   // Find the executed tests in the two test suites.
   TestTree test_trees[2]{TestTree(_dir_left), TestTree(_dir_right)};
@@ -488,15 +489,15 @@ ExitStatus make_comparison(const char* const _dir_left,
     // Create stream to store the differences for the current test
     Base::OutputStreamAdaptT<TestStream> test_diff_log;
 
-    // Get the paths to the reports for both suites
-    const auto report_path_left =
-        test_trees[0].root_dir() / test_path / Test::REPORT_FILENAME;
-    const auto report_path_right =
-        test_trees[1].root_dir() / test_path / Test::REPORT_FILENAME;
+    // Set comparison parameters
+    Test::Checksum::compare.set_reports(
+        test_trees[0].root_dir() / test_path / Test::REPORT_FILENAME,
+        test_trees[1].root_dir() / test_path / Test::REPORT_FILENAME);
+
+    Test::Checksum::compare.set_short_format(_cot == COT_SHORT_DIFF);
 
     // Compare the checksums for the test between the two suites
-    const auto diff_stats = compare_reports(report_path_left, report_path_right,
-        test_diff_log, _chks_cmpr, _cot == COT_SHORT_DIFF);
+    const auto diff_stats = Test::Checksum::compare.run(test_diff_log);
 
     if (!diff_stats.empty())
     { // Differences were found
@@ -648,8 +649,7 @@ ExitStatus make_comparison(const char* const _dir_left,
     continue; \
   }
 
-int report(const int _argc, const char* const _argv[],
-    const Checksum::Compare& _chks_cmpr)
+int report(const int _argc, const char* const _argv[])
 {
   // Set usage text
   const auto usage =
@@ -712,7 +712,7 @@ int report(const int _argc, const char* const _argv[],
   // Perform the appropriate comparison and catch any errors
   try
   {
-    rslt = make_comparison(dir_left, dir_right, cot, _chks_cmpr, show_progress);
+    rslt = make_comparison(dir_left, dir_right, cot, show_progress);
   }
   catch (const std::exception& excpt)
   {
diff --git a/Test/TestReport.hh b/Test/TestReport.hh
index 7b660b0..39e411f 100644
--- a/Test/TestReport.hh
+++ b/Test/TestReport.hh
@@ -54,7 +54,7 @@ of progress-related output from the function.
 */
 ExitStatus make_comparison(const char* const _dir_left,
     const char* const _dir_right, const CompareOutputType _cot,
-    const Checksum::Compare& _chks_cmpr, const bool _show_progress = true);
+    const bool _show_progress = true);
 
 /*!
 This is a wrapper to make_comparison() that parses command-line arguments. The
@@ -66,8 +66,7 @@ following command-line format is expected:
 * --no-progress will set _show_progress = false.
 
 */
-int report(const int _argc, const char* const _argv[],
-    const Checksum::Compare& _chks_cmpr);
+int report(const int _argc, const char* const _argv[]);
 
 } // namespace Test
 
diff --git a/Test/TestResultAnalysis.hh b/Test/TestResultAnalysis.hh
deleted file mode 100644
index 65ceec1..0000000
--- a/Test/TestResultAnalysis.hh
+++ /dev/null
@@ -1,42 +0,0 @@
-// (C) Copyright 2021 by Autodesk, Inc.
-
-#ifndef BASE_TESTRESULTANALYSIS_HH_INCLUDE
-#define BASE_TESTRESULTANALYSIS_HH_INCLUDE
-
-#ifdef TEST_ON
-
-#include <Base/Test/TestChecksumCompare.hh>
-#include <Base/Paths/Filesystem.hh>
-
-#include <map>
-#include <string>
-
-namespace Test
-{
-namespace fs = Base::filesystem;
-
-typedef Checksum::Difference Difference;
-typedef std::map<Difference, size_t> DifferenceDistribution;
-
-/*!
-Compares the checksums listed in two test reports. Returns a map with the
-difference statistics (counts for each type of difference). Equal checksums are
-ignored if _short_frmt = true.
-
-Arguments:
-
-* [in] _rprt_path0: Path to the left-suite report file.
-* [in] _rprt_path1: Path to the right-suite report file.
-* [out] _log: Description of the differences.
-* [in] _chks_cmpr: Checksum compare function.
-* [in] _short_frmt: Remove identical checksums (short format) [default = false].
-
-*/
-DifferenceDistribution compare_reports(const fs::path& _rprt_path0,
-    const fs::path& _rprt_path1, Base::IOutputStream& _log,
-    const Checksum::Compare& _chks_cmpr, const bool _short_frmt = false);
-
-} // namespace Test
-
-#endif // TEST_ON
-#endif // BASE_TESTRESULTANALYSIS_HH_INCLUDE
diff --git a/Utils/CMakeLists.txt b/Utils/CMakeLists.txt
index cf4fb2d..331c336 100644
--- a/Utils/CMakeLists.txt
+++ b/Utils/CMakeLists.txt
@@ -4,6 +4,7 @@ set(my_headers
    ${CMAKE_CURRENT_SOURCE_DIR}/Environment.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/FileOutput.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/IOutputStream.hh
+   ${CMAKE_CURRENT_SOURCE_DIR}/NullOutputStream.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/OStringStream.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/RedirectStream.hh
    ${CMAKE_CURRENT_SOURCE_DIR}/StopWatch.hh
@@ -17,6 +18,7 @@ set(my_sources
    ${CMAKE_CURRENT_SOURCE_DIR}/Environment.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/FileOutput.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/IOutputStream.cc
+   ${CMAKE_CURRENT_SOURCE_DIR}/NullOutputStream.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/OStringStream.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/RedirectStream.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/StopWatch.cc
diff --git a/Utils/NullOutputStream.cc b/Utils/NullOutputStream.cc
new file mode 100755
index 0000000..2f522ef
--- /dev/null
+++ b/Utils/NullOutputStream.cc
@@ -0,0 +1,11 @@
+// (C) Copyright 2021 by Autodesk, Inc.
+
+#include "Base/Security/Mandatory.hh"
+#include "NullOutputStream.hh"
+
+namespace Base
+{
+
+NullOutputStream null_os;
+
+} // namespace Base
diff --git a/Utils/NullOutputStream.hh b/Utils/NullOutputStream.hh
new file mode 100755
index 0000000..3f9dce6
--- /dev/null
+++ b/Utils/NullOutputStream.hh
@@ -0,0 +1,27 @@
+// (C) Copyright 2021 by Autodesk, Inc.
+
+#ifndef BASE_NULLOUTPUTSTREAM_HH_INCLUDED
+#define BASE_NULLOUTPUTSTREAM_HH_INCLUDED
+
+#include <Base/Utils/IOutputStream.hh>
+
+namespace Base
+{
+
+// An output stream that ignores data streamed to it
+class BASEDLLEXPORT NullOutputStream : public IOutputStream
+{
+public:
+  IOutputStream& print(const char) override { return *this; }
+  IOutputStream& print(const int) override { return *this; }
+  IOutputStream& print(const size_t) override { return *this; }
+  IOutputStream& print(const float) override { return *this; }
+  IOutputStream& print(const double) override { return *this; }
+  IOutputStream& print(const char* const) override { return *this; }
+};
+
+extern NullOutputStream null_os;
+
+} // namespace Base
+
+#endif // BASE_NULLOUTPUTSTREAM_HH_INCLUDED
-- 
GitLab