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 }