github.com/swiftstack/proxyfs@v0.0.0-20201223034610-5434d919416e/bucketstats/api_test.go (about)

     1  package bucketstats
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"regexp"
     7  	"testing"
     8  )
     9  
    10  // a structure containing all of the bucketstats statistics types and other
    11  // fields; useful for testing
    12  type allStatTypes struct {
    13  	MyName         string // not a statistic
    14  	bar            int    // also not a statistic
    15  	Total1         Total
    16  	Average1       Average
    17  	BucketLog2     BucketLog2Round
    18  	BucketLogRoot2 BucketLogRoot2Round
    19  }
    20  
    21  func TestTables(t *testing.T) {
    22  	// showDistr(log2RoundIdxTable[:])
    23  	// showDistr(logRoot2RoundIdxTable[:])
    24  
    25  	// generate the tables (arrays) for tables.go.  the tables are already
    26  	// there, but this is where they came from.  the stdout could be cut and
    27  	// pasted at the end of tables.go.
    28  	//
    29  	//genLog2Table()
    30  	//genLogRoot2Table()
    31  }
    32  
    33  // verify that all of the bucketstats statistics types satisfy the appropriate
    34  // interface (this is really a compile time test; it fails if they don't)
    35  func TestBucketStatsInterfaces(t *testing.T) {
    36  	var (
    37  		Total1         Total
    38  		Average1       Average
    39  		BucketLogRoot2 BucketLog2Round
    40  		BucketRoot2    BucketLogRoot2Round
    41  		TotalIface     Totaler
    42  		AverageIface   Averager
    43  		BucketIface    Bucketer
    44  	)
    45  
    46  	// all the types are Totaler(s)
    47  	TotalIface = &Total1
    48  	TotalIface = &Average1
    49  	TotalIface = &BucketLogRoot2
    50  	TotalIface = &BucketRoot2
    51  
    52  	// most of the types are also Averager(s)
    53  	AverageIface = &Average1
    54  	AverageIface = &BucketLogRoot2
    55  	AverageIface = &BucketRoot2
    56  
    57  	// and the bucket types are Bucketer(s)
    58  	BucketIface = &BucketLogRoot2
    59  	BucketIface = &BucketRoot2
    60  
    61  	// keep the compiler happy by doing something with the local variables
    62  	AverageIface = BucketIface
    63  	TotalIface = AverageIface
    64  	_ = TotalIface
    65  }
    66  
    67  func TestRegister(t *testing.T) {
    68  
    69  	var (
    70  		testFunc func()
    71  		panicStr string
    72  	)
    73  
    74  	// registering a struct with all of the statist types should not panic
    75  	var myStats allStatTypes = allStatTypes{
    76  		Total1:         Total{Name: "mytotaler"},
    77  		Average1:       Average{Name: "First_Average"},
    78  		BucketLog2:     BucketLog2Round{Name: "bucket_log2"},
    79  		BucketLogRoot2: BucketLogRoot2Round{Name: "bucket_logroot2"},
    80  	}
    81  	Register("main", "myStats", &myStats)
    82  
    83  	// unregister-ing and re-register-ing myStats is also fine
    84  	UnRegister("main", "myStats")
    85  	Register("main", "myStats", &myStats)
    86  
    87  	// its also OK to unregister stats that don't exist
    88  	UnRegister("main", "neverStats")
    89  
    90  	// but registering it twice should panic
    91  	testFunc = func() {
    92  		Register("main", "myStats", &myStats)
    93  	}
    94  	panicStr = catchAPanic(testFunc)
    95  	if panicStr == "" {
    96  		t.Errorf("Register() of \"main\", \"myStats\" twice should have paniced")
    97  	}
    98  	UnRegister("main", "myStats")
    99  
   100  	// a statistics group must have at least one of package and group name
   101  	UnRegister("main", "myStats")
   102  
   103  	Register("", "myStats", &myStats)
   104  	UnRegister("", "myStats")
   105  
   106  	Register("main", "", &myStats)
   107  	UnRegister("main", "")
   108  
   109  	testFunc = func() {
   110  		Register("", "", &myStats)
   111  	}
   112  	panicStr = catchAPanic(testFunc)
   113  	if panicStr == "" {
   114  		t.Errorf("Register() of statistics group without a name didn't panic")
   115  	}
   116  
   117  	// Registering a struct without any bucketstats statistics is also OK
   118  	emptyStats := struct {
   119  		someInt    int
   120  		someString string
   121  		someFloat  float64
   122  	}{}
   123  
   124  	testFunc = func() {
   125  		Register("main", "emptyStats", &emptyStats)
   126  	}
   127  	panicStr = catchAPanic(testFunc)
   128  	if panicStr != "" {
   129  		t.Errorf("Register() of struct without statistics paniced: %s", panicStr)
   130  	}
   131  
   132  	// Registering unnamed and uninitialized statistics should name and init
   133  	// them, but not change the name if one is already assigned
   134  	var myStats2 allStatTypes = allStatTypes{}
   135  	Register("main", "myStats2", &myStats2)
   136  	if myStats2.Total1.Name != "Total1" || myStats.Total1.Name != "mytotaler" {
   137  		t.Errorf("After Register() a Totaler name is incorrect '%s' or '%s'",
   138  			myStats2.Total1.Name, myStats.Total1.Name)
   139  	}
   140  	if myStats2.Average1.Name != "Average1" || myStats.Average1.Name != "First_Average" {
   141  		t.Errorf("After Register() an Average name is incorrect '%s' or '%s'",
   142  			myStats2.Average1.Name, myStats.Average1.Name)
   143  	}
   144  	if myStats2.BucketLog2.Name != "BucketLog2" || myStats.BucketLog2.Name != "bucket_log2" {
   145  		t.Errorf("After Register() an Average name is incorrect '%s' or '%s'",
   146  			myStats2.BucketLog2.Name, myStats.BucketLog2.Name)
   147  	}
   148  	if myStats2.BucketLogRoot2.Name != "BucketLogRoot2" || myStats.BucketLogRoot2.Name != "bucket_logroot2" {
   149  		t.Errorf("After Register() an Average name is incorrect '%s' or '%s'",
   150  			myStats2.BucketLogRoot2.Name, myStats.BucketLogRoot2.Name)
   151  	}
   152  	// (values are somewhat arbitrary and can change)
   153  	if myStats2.BucketLog2.NBucket != 65 || myStats2.BucketLogRoot2.NBucket != 128 {
   154  		t.Errorf("After Register() NBucket was not initialized got %d and %d",
   155  			myStats2.BucketLog2.NBucket, myStats2.BucketLogRoot2.NBucket)
   156  	}
   157  	UnRegister("main", "myStats2")
   158  
   159  	// try with minimal number of buckets
   160  	var myStats3 allStatTypes = allStatTypes{
   161  		BucketLog2:     BucketLog2Round{NBucket: 1},
   162  		BucketLogRoot2: BucketLogRoot2Round{NBucket: 1},
   163  	}
   164  	Register("main", "myStats3", &myStats3)
   165  	// (minimum number of buckets is somewhat arbitrary and may change)
   166  	if myStats3.BucketLog2.NBucket != 10 || myStats3.BucketLogRoot2.NBucket != 17 {
   167  		t.Errorf("After Register() NBucket was not initialized got %d and %d",
   168  			myStats3.BucketLog2.NBucket, myStats3.BucketLogRoot2.NBucket)
   169  	}
   170  	UnRegister("main", "myStats3")
   171  
   172  	// two fields with the same name ("Average1") will panic
   173  	var myStats4 allStatTypes = allStatTypes{
   174  		Total1:     Total{Name: "mytotaler"},
   175  		Average1:   Average{},
   176  		BucketLog2: BucketLog2Round{Name: "Average1"},
   177  	}
   178  	testFunc = func() {
   179  		Register("main", "myStats4", &myStats4)
   180  	}
   181  	panicStr = catchAPanic(testFunc)
   182  	if panicStr == "" {
   183  		t.Errorf("Register() of struct with duplicate field names should panic")
   184  	}
   185  }
   186  
   187  // All of the bucketstats statistics are Totaler(s); test them
   188  func TestTotaler(t *testing.T) {
   189  	var (
   190  		totaler         Totaler
   191  		totalerGroup    allStatTypes = allStatTypes{}
   192  		totalerGroupMap map[string]Totaler
   193  		name            string
   194  		total           uint64
   195  	)
   196  
   197  	totalerGroupMap = map[string]Totaler{
   198  		"Total":          &totalerGroup.Total1,
   199  		"Average":        &totalerGroup.Average1,
   200  		"BucketLog2":     &totalerGroup.BucketLog2,
   201  		"BucketLogRoot2": &totalerGroup.BucketLogRoot2,
   202  	}
   203  
   204  	// must be registered (inited) before use
   205  	Register("main", "TotalerStat", &totalerGroup)
   206  
   207  	// all totalers should start out at 0
   208  	for name, totaler = range totalerGroupMap {
   209  		if totaler.TotalGet() != 0 {
   210  			t.Errorf("%s started at total %d instead of 0", name, totaler.TotalGet())
   211  		}
   212  	}
   213  
   214  	// after incrementing twice they should be 2
   215  	for _, totaler = range totalerGroupMap {
   216  		totaler.Increment()
   217  		totaler.Increment()
   218  	}
   219  	for name, totaler = range totalerGroupMap {
   220  		if totaler.TotalGet() != 2 {
   221  			t.Errorf("%s at total %d instead of 2 after 2 increments", name, totaler.TotalGet())
   222  		}
   223  	}
   224  
   225  	// after adding 0 total should still be 2
   226  	for _, totaler = range totalerGroupMap {
   227  		totaler.Add(0)
   228  		totaler.Add(0)
   229  	}
   230  	for name, totaler = range totalerGroupMap {
   231  		if totaler.TotalGet() != 2 {
   232  			t.Errorf("%s got total %d instead of 2 after adding 0", name, totaler.TotalGet())
   233  		}
   234  	}
   235  
   236  	// after adding 4 and 8 they must all total to 14
   237  	//
   238  	// (this does not work when adding values larger than 8 where the mean
   239  	// value of buckets for bucketized statistics diverges from the nominal
   240  	// value, i.e. adding 64 will produce totals of 70 for BucketLog2 and 67
   241  	// for BucketLogRoot2 because the meanVal for the bucket 64 is put in
   242  	// are 68 and 65, respectively)
   243  	for _, totaler = range totalerGroupMap {
   244  		totaler.Add(4)
   245  		totaler.Add(8)
   246  	}
   247  	for name, totaler = range totalerGroupMap {
   248  		if totaler.TotalGet() != 14 {
   249  			t.Errorf("%s at total %d instead of 6 after adding 4 and 8", name, totaler.TotalGet())
   250  		}
   251  	}
   252  
   253  	// Sprint for each should do something for all stats types
   254  	// (not really making the effort to parse the string)
   255  	for name, totaler = range totalerGroupMap {
   256  		prettyPrint := totaler.Sprint(StatFormatParsable1, "main", "TestTotaler")
   257  		if prettyPrint == "" {
   258  			t.Errorf("%s returned an empty string for its Sprint() method", name)
   259  		}
   260  	}
   261  
   262  	// The Total returned for bucketized statistics will vary depending on
   263  	// the actual numbers used can can be off by more then 33% (Log2) or
   264  	// 17.2% (LogRoot2) in the worst case, and less in the average case.
   265  	//
   266  	// Empirically for 25 million runs using 1024 numbers each the error is
   267  	// no more than 10.0% (Log2 buckets) and 5.0% (LogRoot2 buckets).
   268  	//
   269  	// Run the test 1000 times -- note that go produces the same sequence of
   270  	// "random" numbers each time for the same seed, so statistical variation
   271  	// is not going to cause random test failures.
   272  	var (
   273  		log2RoundErrorPctMax        float64 = 33.3333333333333
   274  		log2RoundErrorPctLikely     float64 = 10
   275  		logRoot2RoundErrorPctMax    float64 = 17.241379310
   276  		logRoot2RoundErrorPctLikely float64 = 5.0
   277  	)
   278  
   279  	rand.Seed(2)
   280  	for loop := 0; loop < 1000; loop++ {
   281  
   282  		var (
   283  			newTotalerGroup allStatTypes
   284  			errPct          float64
   285  		)
   286  
   287  		totalerGroupMap = map[string]Totaler{
   288  			"Total":          &newTotalerGroup.Total1,
   289  			"Average":        &newTotalerGroup.Average1,
   290  			"BucketLog2":     &newTotalerGroup.BucketLog2,
   291  			"BucketLogRoot2": &newTotalerGroup.BucketLogRoot2,
   292  		}
   293  
   294  		// newTotalerGroup must be registered (inited) before use
   295  
   296  		UnRegister("main", "TotalerStat")
   297  		Register("main", "TotalerStat", &newTotalerGroup)
   298  
   299  		// add 1,0240 random numbers uniformly distributed [0, 6106906623)
   300  		//
   301  		// 6106906623 is RangeHigh for bucket 33 of BucketLog2Round and
   302  		// 5133828095 is RangeHigh for bucket 64 of BucketLogRoot2Round;
   303  		// using 5133828095 makes BucketLogRoot2Round look better and
   304  		// BucketLog2Round look worse.
   305  		total = 0
   306  		for i := 0; i < 1024; i++ {
   307  			randVal := uint64(rand.Int63n(6106906623))
   308  			//randVal := uint64(rand.Int63n(5133828095))
   309  
   310  			total += randVal
   311  			for _, totaler = range totalerGroupMap {
   312  				totaler.Add(randVal)
   313  			}
   314  		}
   315  
   316  		// validate total for each statistic; barring a run of extremely
   317  		// bad luck we expect the bucket stats will be less then
   318  		// log2RoundErrorPctLikely and logRoot2RoundErrorPctLikely,
   319  		// respectively
   320  		if newTotalerGroup.Total1.TotalGet() != total {
   321  			t.Errorf("Total1 total is %d instead of %d", newTotalerGroup.Total1.TotalGet(), total)
   322  		}
   323  		if newTotalerGroup.Average1.TotalGet() != total {
   324  			t.Errorf("Average1 total is %d instead of %d", newTotalerGroup.Average1.TotalGet(), total)
   325  		}
   326  
   327  		errPct = (float64(newTotalerGroup.BucketLog2.TotalGet())/float64(total) - 1) * 100
   328  		if errPct > log2RoundErrorPctMax || errPct < -log2RoundErrorPctMax {
   329  			t.Fatalf("BucketLog2Round total exceeds maximum possible error 33%%: "+
   330  				"%d instead of %d  error %1.3f%%",
   331  				newTotalerGroup.BucketLog2.TotalGet(), total, errPct)
   332  
   333  		}
   334  		if errPct > log2RoundErrorPctLikely || errPct < -log2RoundErrorPctLikely {
   335  			t.Errorf("BucketLog2Round total exceeds maximum likely error: %d instead of %d  error %1.3f%%",
   336  				newTotalerGroup.BucketLog2.TotalGet(), total, errPct)
   337  		}
   338  
   339  		errPct = (float64(newTotalerGroup.BucketLogRoot2.TotalGet())/float64(total) - 1) * 100
   340  		if errPct > logRoot2RoundErrorPctMax || errPct < -logRoot2RoundErrorPctMax {
   341  			t.Fatalf("BucketLogRoot2Round total exceeds maximum possible error 17.2%%: "+
   342  				"%d instead of %d  error %1.3f%%",
   343  				newTotalerGroup.BucketLogRoot2.TotalGet(), total, errPct)
   344  		}
   345  		if errPct > logRoot2RoundErrorPctLikely || errPct < -logRoot2RoundErrorPctLikely {
   346  			t.Errorf("BucketLogRoot2Round total exceeds maximum likely error: "+
   347  				"%d instead of %d  error %1.3f%%",
   348  				newTotalerGroup.BucketLogRoot2.TotalGet(), total, errPct)
   349  		}
   350  
   351  	}
   352  
   353  	// Sprint for each should do something for all statistic types
   354  	// (without trying to validate the string)
   355  	for name, totaler = range totalerGroupMap {
   356  		prettyPrint := totaler.Sprint(StatFormatParsable1, "main", "TestTotaler")
   357  		if prettyPrint == "" {
   358  			t.Errorf("%s returned an empty string for its Sprint() method", name)
   359  		}
   360  	}
   361  }
   362  
   363  // Test Bucketer specific functionality (which is mostly buckets)
   364  //
   365  func TestBucketer(t *testing.T) {
   366  
   367  	var (
   368  		bucketerGroup    allStatTypes = allStatTypes{}
   369  		bucketerGroupMap map[string]Bucketer
   370  		bucketInfoTmp    []BucketInfo
   371  		bucketer         Bucketer
   372  		name             string
   373  		//total              uint64
   374  	)
   375  
   376  	// describe the buckets for testing
   377  	bucketerGroupMap = map[string]Bucketer{
   378  		"BucketLog2":     &bucketerGroup.BucketLog2,
   379  		"BucketLogRoot2": &bucketerGroup.BucketLogRoot2,
   380  	}
   381  
   382  	// buckets must be registered (inited) before use
   383  	Register("main", "BucketerStat", &bucketerGroup)
   384  
   385  	// verify that each type has the right number of buckets, where "right"
   386  	// is implementation defined
   387  	if len(bucketerGroup.BucketLog2.DistGet()) != 65 {
   388  		t.Errorf("BucketLog2 has %d buckets should be 65", bucketerGroup.BucketLog2.DistGet())
   389  	}
   390  	if len(bucketerGroup.BucketLogRoot2.DistGet()) != 128 {
   391  		t.Errorf("BucketLog2 has %d buckets should be 128", bucketerGroup.BucketLog2.DistGet())
   392  	}
   393  
   394  	// all buckets should start out at 0
   395  	for name, bucketer = range bucketerGroupMap {
   396  
   397  		if bucketer.TotalGet() != 0 {
   398  			t.Errorf("%s started at total %d instead of 0", name, bucketer.TotalGet())
   399  		}
   400  		if bucketer.AverageGet() != 0 {
   401  			t.Errorf("%s started at average %d instead of 0", name, bucketer.AverageGet())
   402  		}
   403  
   404  		bucketInfoTmp = bucketer.DistGet()
   405  		for bucketIdx := 0; bucketIdx < len(bucketInfoTmp); bucketIdx++ {
   406  			if bucketInfoTmp[bucketIdx].Count != 0 {
   407  				t.Errorf("%s started out with bucket[%d].Count is %d instead of 0",
   408  					name, bucketIdx, bucketInfoTmp[bucketIdx].Count)
   409  			}
   410  		}
   411  	}
   412  
   413  	// verify that RangeLow, RangeHigh, NominalVal, and MeanVal are all
   414  	// placed in the the bucket that they should be
   415  	for name, bucketer = range bucketerGroupMap {
   416  
   417  		bucketInfoTmp = bucketer.DistGet()
   418  		for bucketIdx := 0; bucketIdx < len(bucketInfoTmp); bucketIdx++ {
   419  			bucketer.Add(bucketInfoTmp[bucketIdx].RangeLow)
   420  			bucketer.Add(bucketInfoTmp[bucketIdx].RangeHigh)
   421  			bucketer.Add(bucketInfoTmp[bucketIdx].NominalVal)
   422  			bucketer.Add(bucketInfoTmp[bucketIdx].MeanVal)
   423  
   424  			if bucketer.DistGet()[bucketIdx].Count != 4 {
   425  				t.Errorf("%s added 4 values to index %d but got %d",
   426  					name, bucketIdx, bucketer.DistGet()[bucketIdx].Count)
   427  			}
   428  		}
   429  	}
   430  
   431  	// verify RangeLow and RangeHigh are contiguous
   432  	for name, bucketer = range bucketerGroupMap {
   433  
   434  		// at least for these buckets, the first bucket always maps only 0
   435  		if bucketInfoTmp[0].RangeLow != 0 && bucketInfoTmp[0].RangeHigh != 0 {
   436  			t.Errorf("%s bucket 0 RangeLow %d or RangeHigh %d does not match 0",
   437  				name, bucketInfoTmp[0].RangeLow, bucketInfoTmp[0].RangeHigh)
   438  		}
   439  
   440  		lastRangeHigh := uint64(0)
   441  		bucketInfoTmp = bucketer.DistGet()
   442  		for bucketIdx := 1; bucketIdx < len(bucketInfoTmp); bucketIdx++ {
   443  			if bucketInfoTmp[bucketIdx].RangeLow != lastRangeHigh+1 {
   444  				t.Errorf("%s bucket %d RangeLow %d does not match last RangeHigh %d",
   445  					name, bucketIdx, bucketInfoTmp[bucketIdx].RangeLow, lastRangeHigh+1)
   446  			}
   447  			lastRangeHigh = bucketInfoTmp[bucketIdx].RangeHigh
   448  		}
   449  
   450  		if lastRangeHigh != (1<<64)-1 {
   451  			t.Errorf("%s last bucket RangeHigh is %d instead of %d", name, lastRangeHigh, uint64((1<<64)-1))
   452  		}
   453  	}
   454  
   455  	for name, bucketer = range bucketerGroupMap {
   456  		prettyPrint := bucketer.Sprint(StatFormatParsable1, "main", "BucketGroup")
   457  		if prettyPrint == "" {
   458  			t.Errorf("%s returned an empty string for its Sprint() method", name)
   459  		}
   460  	}
   461  
   462  }
   463  
   464  func TestSprintStats(t *testing.T) {
   465  
   466  	// array of all valid StatStringFormat
   467  	statFmtList := []StatStringFormat{StatFormatParsable1}
   468  
   469  	var (
   470  		testFunc func()
   471  		panicStr string
   472  		statFmt  StatStringFormat
   473  	)
   474  
   475  	// sprint'ing unregistered stats group should panic
   476  	testFunc = func() {
   477  		fmt.Print(SprintStats(statFmt, "main", "no-such-stats"))
   478  	}
   479  	panicStr = catchAPanic(testFunc)
   480  	if panicStr == "" {
   481  		t.Errorf("SprintStats() of unregistered statistic group did not panic")
   482  	}
   483  
   484  	// verify StatFormatParsable1, and other formats, handle illegal
   485  	// characters in names (StatFormatParsable1 replaces them with
   486  	// underscore ('_'))
   487  	for _, statFmt = range statFmtList {
   488  
   489  		var myStats5 allStatTypes = allStatTypes{
   490  			Total1:         Total{Name: ":colon #sharp \nNewline \tTab \bBackspace-are-changed"},
   491  			Average1:       Average{Name: "and*splat*is*also*changed"},
   492  			BucketLog2:     BucketLog2Round{Name: "spaces get replaced as well"},
   493  			BucketLogRoot2: BucketLogRoot2Round{Name: "but !@$%^&()[]` are OK"},
   494  		}
   495  		statisticNamesScrubbed := []string{
   496  			"_colon__sharp__Newline__Tab__Backspace-are-changed",
   497  			"and_splat_is_also_changed",
   498  			"spaces_get_replaced_as_well",
   499  			"but_!@$%^&()[]`_are_OK",
   500  		}
   501  
   502  		pkgName := "m*a:i#n"
   503  		pkgNameScrubbed := "m_a_i_n"
   504  		statsGroupName := "m y s t a t s 5"
   505  		statsGroupNameScrubbed := "m_y_s_t_a_t_s_5"
   506  
   507  		Register(pkgName, statsGroupName, &myStats5)
   508  
   509  		switch statFmt {
   510  
   511  		default:
   512  			t.Fatalf("SprintStats(): unknown StatStringFormat %v\n", statFmt)
   513  
   514  		case StatFormatParsable1:
   515  			statsString := SprintStats(StatFormatParsable1, pkgName, statsGroupName)
   516  			if statsString == "" {
   517  				t.Fatalf("SprintStats(%s, %s,) did not find the statsgroup", pkgName, statsGroupName)
   518  			}
   519  
   520  			rowDelimiterRE := regexp.MustCompile("\n")
   521  			fieldDelimiterRe := regexp.MustCompile(" +")
   522  
   523  			for _, row := range rowDelimiterRE.Split(statsString, -1) {
   524  				if row == "" {
   525  					continue
   526  				}
   527  
   528  				statisticName := fieldDelimiterRe.Split(row, 2)[0]
   529  				matched := false
   530  				for _, scrubbedName := range statisticNamesScrubbed {
   531  					fu := pkgNameScrubbed + "." + statsGroupNameScrubbed + "." + scrubbedName
   532  					if statisticName == fu {
   533  						matched = true
   534  						break
   535  					}
   536  				}
   537  
   538  				if !matched {
   539  					t.Errorf("TestSprintStats: statisticName '%s' did not match any statistic name",
   540  						statisticName)
   541  				}
   542  			}
   543  		}
   544  		UnRegister("m*a:i#n", "m y s t a t s 5")
   545  	}
   546  }
   547  
   548  // Invoke function aFunc, which is expected to panic.  If it does, return the
   549  // value returned by recover() as a string, otherwise return the empty string.
   550  //
   551  // If panic() is called with a nil argument then this function also returns the
   552  // empty string.
   553  //
   554  func catchAPanic(aFunc func()) (panicStr string) {
   555  
   556  	defer func() {
   557  		// if recover() returns !nil then return it as a string
   558  		panicVal := recover()
   559  		if panicVal != nil {
   560  			panicStr = fmt.Sprintf("%v", panicVal)
   561  		}
   562  	}()
   563  
   564  	aFunc()
   565  	return
   566  }