[beast: 4/15] SFI: testing: add Test utilities for unit tests



commit c950298b6ee64c206e78b4e99d5625153d67dc7f
Author: Tim Janik <timj gnu org>
Date:   Sun Jul 23 00:59:20 2017 +0200

    SFI: testing: add Test utilities for unit tests
    
    Signed-off-by: Tim Janik <timj gnu org>

 sfi/Makefile.am |    4 +-
 sfi/testing.cc  |  233 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 sfi/testing.hh  |  128 ++++++++++++++++++++++++++++++
 3 files changed, 364 insertions(+), 1 deletions(-)
---
diff --git a/sfi/Makefile.am b/sfi/Makefile.am
index 686ef65..ed8494b 100644
--- a/sfi/Makefile.am
+++ b/sfi/Makefile.am
@@ -15,8 +15,9 @@ sfi_public_headers = $(strip \
        sfitime.hh      sfitypes.hh     sfivalues.hh    \
        sfivisitors.hh  sfiustore.hh    \
        sficxx.hh       sfiring.hh      sfimemory.hh    sficomport.hh   \
-       sfitests.hh     sfi.hh                                          \
+                       sfi.hh                                          \
        gbsearcharray.hh \
+       testing.hh      \
 )
 sfi_all_sources = $(strip \
        bcore.cc        formatter.cc    path.cc         platform.cc     strings.cc      \
@@ -27,6 +28,7 @@ sfi_all_sources = $(strip \
        sfitime.cc      sfitypes.cc     sfivalues.cc    \
        sfivisitors.cc  sfiustore.cc    \
                        sfiring.cc      sfimemory.cc    sficomport.cc   \
+       testing.cc      \
 )
 sfi_extra_sources = $(strip \
 )
diff --git a/sfi/testing.cc b/sfi/testing.cc
new file mode 100644
index 0000000..16c9431
--- /dev/null
+++ b/sfi/testing.cc
@@ -0,0 +1,233 @@
+// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
+#include "testing.hh"
+#include <algorithm>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#define TDEBUG(...)     Bse::debug ("Test", __VA_ARGS__)
+
+namespace Bse {
+
+/** The Test namespace offers utilities for unit tests.
+ * The Test namespace is made available by <code> \#include <sfi/testing.hh> </code> <br/>
+ * See also sfi/testing.hh.
+ */
+namespace Test {
+
+static void
+test_assertion_failed()
+{
+  void *__p_[BSE_BACKTRACE_MAXDEPTH] = { 0, };
+  String btmsg = pretty_backtrace (__p_, backtrace_pointers (__p_, sizeof (__p_) / sizeof (__p_[0])), 
__FILE__, __LINE__, NULL);
+  if (btmsg.size())
+    printerr ("%s", btmsg.c_str());
+  printerr ("Rapicorn::test_assertion_failed(): aborting...");
+  Bse::breakpoint();
+  abort();
+}
+
+/** Initialize the Bse core for a test program.
+ * Initializes Bse to execute unit tests by calling parse_settings_and_args()
+ * with args "autonomous=1" and "testing=1" and setting the application name.
+ * See also #$BSE_DEBUG.
+ */
+void
+init (int *argcp, char **argv, const StringVector &args)
+{
+  Aida::assertion_failed_hook (test_assertion_failed);
+  unsigned int flags = g_log_set_always_fatal (GLogLevelFlags (G_LOG_FATAL_MASK));
+  g_log_set_always_fatal (GLogLevelFlags (flags | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL));
+  sfi_init (argcp, argv, Bse::cstrings_to_vector (":autonomous:testing:fatal-warnings:", NULL));
+}
+
+Timer::Timer (double deadline_in_secs) :
+  deadline_ (deadline_in_secs), test_duration_ (0), n_reps_ (0)
+{}
+
+Timer::~Timer ()
+{}
+
+double
+Timer::bench_time ()
+{
+  /* timestamp_benchmark() counts nano seconds since program start, so
+   * it's not going to exceed the 52bit double mantissa too fast.
+   */
+  return timestamp_benchmark() / 1000000000.0;
+}
+
+#define DEBUG_LOOPS_NEEDED(...) while (0) printerr (__VA_ARGS__)
+
+int64
+Timer::loops_needed ()
+{
+  if (samples_.size() < 3)
+    {
+      n_reps_ = MAX (1, n_reps_);
+      DEBUG_LOOPS_NEEDED ("loops_needed: %d\n", n_reps_);
+      return n_reps_;           // force significant number of test runs
+    }
+  double resolution = timestamp_resolution() / 1000000000.0;
+  const double deadline = MAX (deadline_ == 0.0 ? 0.005 : deadline_, resolution * 10000.0);
+  if (test_duration_ < deadline * 0.2)
+    {
+      // increase the number of tests per run to gain more accuracy
+      n_reps_ = MAX (n_reps_ + 1, int64 (n_reps_ * 1.5)) | 1;
+      DEBUG_LOOPS_NEEDED ("loops_needed: %d\n", n_reps_);
+      return n_reps_;
+    }
+  if (test_duration_ < deadline)
+    {
+      DEBUG_LOOPS_NEEDED ("loops_needed: %d\n", n_reps_);
+      return n_reps_;
+    }
+  DEBUG_LOOPS_NEEDED ("loops_needed: %d\n", 0);
+  return 0;
+}
+
+void
+Timer::submit (double elapsed, int64 repetitions)
+{
+  test_duration_ += elapsed;
+  double resolution = timestamp_resolution() / 1000000000.0;
+  if (elapsed >= resolution * 500.0) // force error below 5%
+    samples_.push_back (elapsed / repetitions);
+  else
+    n_reps_ = (n_reps_ + n_reps_) | 1; // double n_reps_ to yield significant times
+}
+
+void
+Timer::reset()
+{
+  samples_.resize (0);
+  test_duration_ = 0;
+  n_reps_ = 0;
+}
+
+double
+Timer::min_elapsed () const
+{
+  double m = DBL_MAX;
+  for (size_t i = 0; i < samples_.size(); i++)
+    m = MIN (m, samples_[i]);
+  return m;
+}
+
+double
+Timer::max_elapsed () const
+{
+  double m = 0;
+  for (size_t i = 0; i < samples_.size(); i++)
+    m = MAX (m, samples_[i]);
+  return m;
+}
+
+static String
+ensure_newline (const String &s)
+{
+  if (!s.size() || s[s.size()-1] != '\n')
+    return s + "\n";
+  return s;
+}
+
+static __thread String *thread_test_start = NULL;
+
+void
+test_output (int kind, const String &msg)
+{
+  if (!thread_test_start)
+    thread_test_start = new String();
+  String &test_start = *thread_test_start;
+  String prefix, sout;
+  bool aborting = false;
+  switch (kind)
+    {
+    case 'S':                   // TSTART()
+      if (!test_start.empty())
+        return test_output ('F', string_format ("Unfinished Test: %s\n", test_start));
+      test_start = msg;
+      sout = "  START…   " + ensure_newline (msg);
+      break;
+    case 'D':                   // TDONE() - test passed
+      if (test_start.empty())
+        return test_output ('F', "Extraneous TDONE() call");
+      test_start = "";
+      sout = "  …DONE    " + ensure_newline (msg);
+      break;
+    case 'I':                   // TNOTE() - verbose test message
+      if (verbose())
+        sout = "  NOTE     " + ensure_newline (msg);
+      break;
+    case 'P':
+      sout = "  PASS     " + ensure_newline (msg);
+      break;
+    case 'F':
+      sout = "  FAIL     " + ensure_newline (msg);
+      aborting = true;
+      break;
+    default:
+      sout = "  INFO     " + ensure_newline (msg);
+      break;
+    }
+  if (!sout.empty())            // test message output
+    {
+      fflush (stderr);
+      fputs (sout.c_str(), stdout);
+      fflush (stdout);
+    }
+  if (aborting)
+    {
+      breakpoint();
+    }
+}
+
+bool
+slow()
+{
+  static bool cached_slow = feature_toggle_bool (getenv ("BSE_TEST"), "slow");
+  return cached_slow;
+}
+
+bool
+verbose()
+{
+  static bool cached_verbose = feature_toggle_bool (getenv ("BSE_TEST"), "verbose");
+  return cached_verbose;
+}
+
+int
+run (void)
+{
+  // FIXME: run_tests();
+  return 0;
+}
+
+uint64_t
+random_int64 ()
+{
+  return Bse::random_int64();
+}
+
+int64_t
+random_irange (int64_t begin, int64_t end)
+{
+  return Bse::random_irange (begin, end);
+}
+
+double
+random_float ()
+{
+  return Bse::random_float();
+}
+
+double
+random_frange (double begin, double end)
+{
+  return Bse::random_frange (begin, end);
+}
+
+} } // Bse::Test
diff --git a/sfi/testing.hh b/sfi/testing.hh
new file mode 100644
index 0000000..56e093e
--- /dev/null
+++ b/sfi/testing.hh
@@ -0,0 +1,128 @@
+// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
+#ifndef __BSE_TESTING_HH__
+#define __BSE_TESTING_HH__
+
+#include <sfi/sfi.hh>
+
+namespace Bse {
+namespace Test {
+
+void init (int *argcp, char **argv, const StringVector &args = StringVector());
+
+
+// Test Macros
+#define TSTART(...)             Bse::Test::test_output ('S', ::Bse::string_format (__VA_ARGS__))  ///< Print 
message once a test case starts.
+#define TDONE()                 Bse::Test::test_output ('D', "")                                  ///< Print 
message for test case end.
+#define TPASS(...)              Bse::Test::test_output ('P', ::Bse::string_format (__VA_ARGS__))  ///< Print 
a message about a passing test.
+#define TNOTE(...)              Bse::Test::test_output ('I', ::Bse::string_format (__VA_ARGS__))  ///< Print 
a message from a test program.
+#define TCHECK(cond, ...)       Bse::Test::test_output (bool (cond) ? 'P' : 'F', \
+                                                        ::Bse::string_format (__VA_ARGS__))       ///< 
Verbose assertion, calls TPASS() on success.
+#define TCMPS(a,cmp,b)          TCMP_op (a,cmp,b,#a,#b,Bse::Test::_as_strptr)                     ///< 
Variant of TCMP() for C strings.
+#define TCMP(a,cmp,b)           TCMP_op (a,cmp,b,#a,#b,)        ///< Compare @a a and @a b according to 
operator @a cmp, verbose on failiure.
+#define TASSERT(cond)           TASSERT__AT (__LINE__, cond)    ///< Unconditional test assertion, enters 
breakpoint if not fullfilled.
+#define TASSERT_AT(LINE, cond)  TASSERT__AT (LINE, cond)        ///< Unconditional test assertion for deputy 
__LINE__.
+#define TOK()                   do {} while (0)                 ///< Deprecated progress indicator, tests 
generally need to run fast.
+
+/** Class for profiling benchmark tests.
+ * UseCase: Benchmarking function implementations, e.g. to compare sorting implementations.
+ */
+class Timer {
+  const double   deadline_;
+  vector<double> samples_;
+  double         test_duration_;
+  int64          n_reps_;
+  int64          loops_needed ();
+  void           reset        ();
+  void           submit       (double elapsed, int64 repetitions);
+  static double  bench_time   ();
+public:
+  /// Create a Timer() instance, specifying an optional upper bound for test durations.
+  explicit       Timer        (double deadline_in_secs = 0);
+  virtual       ~Timer        ();
+  int64          n_reps       () const { return n_reps_; }             ///< Number of benchmark repetitions 
to execute
+  double         test_elapsed () const { return test_duration_; }      ///< Seconds spent in benchmark()
+  double         min_elapsed  () const;         ///< Minimum time benchmarked for a @a callee() call.
+  double         max_elapsed  () const;         ///< Maximum time benchmarked for a @a callee() call.
+  /**
+   * @param callee        A callable function or object.
+   * Method to benchmark the execution time of @a callee.
+   * @returns Minimum runtime in seconds,
+   */
+  template<typename Callee>
+  double         benchmark    (Callee callee);
+};
+
+
+// === test maintenance ===
+int     run                ();  ///< Run all registered tests.
+bool    verbose            ();  ///< Indicates whether tests should run verbosely.
+bool    slow               ();  ///< Indicates whether slow tests should be run.
+void    test_output       (int kind, const String &string);
+
+
+/// == Stringify Args ==
+inline String                   stringify_arg  (const char   *a, const char *str_a) { return a ? 
string_to_cquote (a) : "(__null)"; }
+template<class V> inline String stringify_arg  (const V      *a, const char *str_a) { return string_format 
("%p", a); }
+template<class A> inline String stringify_arg  (const A      &a, const char *str_a) { return str_a; }
+template<> inline String stringify_arg<float>  (const float  &a, const char *str_a) { return string_format 
("%.8g", a); }
+template<> inline String stringify_arg<double> (const double &a, const char *str_a) { return string_format 
("%.17g", a); }
+template<> inline String stringify_arg<bool>   (const bool   &a, const char *str_a) { return string_format 
("%u", a); }
+template<> inline String stringify_arg<int8>   (const int8   &a, const char *str_a) { return string_format 
("%d", a); }
+template<> inline String stringify_arg<int16>  (const int16  &a, const char *str_a) { return string_format 
("%d", a); }
+template<> inline String stringify_arg<int32>  (const int32  &a, const char *str_a) { return string_format 
("%d", a); }
+template<> inline String stringify_arg<int64>  (const int64  &a, const char *str_a) { return string_format 
("%lld", a); }
+template<> inline String stringify_arg<uint8>  (const uint8  &a, const char *str_a) { return string_format 
("0x%02x", a); }
+template<> inline String stringify_arg<uint16> (const uint16 &a, const char *str_a) { return string_format 
("0x%04x", a); }
+template<> inline String stringify_arg<uint32> (const uint32 &a, const char *str_a) { return string_format 
("0x%08x", a); }
+template<> inline String stringify_arg<uint64> (const uint64 &a, const char *str_a) { return string_format 
("0x%08Lx", a); }
+template<> inline String stringify_arg<String> (const String &a, const char *str_a) { return 
string_to_cquote (a); }
+inline const char* _as_strptr (const char *s) { return s; } // implementation detail
+
+// == Deterministic random numbers for tests ===
+uint64_t random_int64           ();                                     ///< Return random int for 
reproduceble tests.
+int64_t  random_irange          (int64_t begin, int64_t end);           ///< Return random int within range 
for reproduceble tests.
+double   random_float           ();                                     ///< Return random double for 
reproduceble tests.
+double   random_frange          (double begin, double end);             ///< Return random double within 
range for reproduceble tests.
+
+
+// == Implementations ==
+/// @cond
+
+#define TASSERT__AT(LINE, cond)                 \
+  do { if (BSE_ISLIKELY (cond)) break;          \
+    ::Bse::assertion_failed (__FILE__, LINE, #cond); } while (0)
+
+#define TCMP_op(a,cmp,b,sa,sb,cast)                                     \
+  do { if (a cmp b) break;                                              \
+    Bse::String __tcmp_va = Bse::Test::stringify_arg (cast (a), #a);    \
+    Bse::String __tcmp_vb = Bse::Test::stringify_arg (cast (b), #b),    \
+                __tcmp_as = Bse::string_format ("'%s %s %s': %s %s %s", \
+                                                sa, #cmp, sb,           \
+                                                __tcmp_va.c_str(),      \
+                                                #cmp,                   \
+                                                __tcmp_vb.c_str());     \
+    ::Bse::assertion_failed (__FILE__, __LINE__, __tcmp_as.c_str());    \
+  } while (0)
+
+template<typename Callee> double
+Timer::benchmark (Callee callee)
+{
+  reset();
+  for (int64 runs = loops_needed(); runs; runs = loops_needed())
+    {
+      int64 n = runs;
+      const double start = bench_time();
+      while (BSE_ISLIKELY (n--))
+        callee();
+      const double stop = bench_time();
+      submit (stop - start, runs);
+    }
+  return min_elapsed();
+}
+
+/// @endcond
+
+} // Test
+} // Bse
+
+#endif /* __BSE_TESTING_HH__ */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]