github.com/swiftstack/ProxyFS@v0.0.0-20210203235616-4017c267d62f/bucketstats/api_test.go (about)

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