maxseen_test.go 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. package observability
  2. import (
  3. "sync"
  4. "sync/atomic"
  5. "testing"
  6. )
  7. func TestMaxSeen_FirstObservationExports(t *testing.T) {
  8. m := NewMaxSeen()
  9. var got string
  10. var val float64
  11. var calls int
  12. m.RecordAndExport("src-A", 1, func(k string, v float64) {
  13. got = k
  14. val = v
  15. calls++
  16. })
  17. if calls != 1 || got != "src-A" || val != 1 {
  18. t.Fatalf("first: want 1 export src-A=1, got calls=%d key=%q val=%v", calls, got, val)
  19. }
  20. if m.Get("src-A") != 1 {
  21. t.Fatalf("Get: want 1, got %d", m.Get("src-A"))
  22. }
  23. }
  24. func TestMaxSeen_StrictlyMonotonic(t *testing.T) {
  25. m := NewMaxSeen()
  26. var calls int
  27. exp := func(string, float64) { calls++ }
  28. m.RecordAndExport("src-A", 5, exp)
  29. m.RecordAndExport("src-A", 3, exp) // smaller: should not export
  30. m.RecordAndExport("src-A", 5, exp) // equal: should not export
  31. m.RecordAndExport("src-A", 7, exp) // larger: should export
  32. m.RecordAndExport("src-A", 2, exp) // smaller: should not export
  33. m.RecordAndExport("src-A", 10, exp) // larger: should export
  34. if calls != 3 {
  35. t.Fatalf("want 3 exports (1, 7, 10), got %d", calls)
  36. }
  37. if m.Get("src-A") != 10 {
  38. t.Fatalf("Get: want 10, got %d", m.Get("src-A"))
  39. }
  40. }
  41. func TestMaxSeen_PerKeyIsolation(t *testing.T) {
  42. m := NewMaxSeen()
  43. exp := func(string, float64) {}
  44. m.RecordAndExport("src-A", 100, exp)
  45. m.RecordAndExport("src-B", 5, exp)
  46. if m.Get("src-A") != 100 {
  47. t.Fatalf("src-A: want 100, got %d", m.Get("src-A"))
  48. }
  49. if m.Get("src-B") != 5 {
  50. t.Fatalf("src-B: want 5, got %d", m.Get("src-B"))
  51. }
  52. if m.Get("src-C") != 0 {
  53. t.Fatalf("src-C (unseen): want 0, got %d", m.Get("src-C"))
  54. }
  55. }
  56. func TestMaxSeen_ConcurrentSameKey(t *testing.T) {
  57. m := NewMaxSeen()
  58. var exports atomic.Int64
  59. exp := func(string, float64) { exports.Add(1) }
  60. var wg sync.WaitGroup
  61. for g := 0; g < 8; g++ {
  62. wg.Add(1)
  63. go func(g int) {
  64. defer wg.Done()
  65. for v := 1; v <= 1000; v++ {
  66. // Each goroutine uses a different value range
  67. // so we know the global max is 8*1000 - 7 = 7993
  68. // (8 goroutines, each writes its own values
  69. // shifted by g*1000). Just write all of them.
  70. _ = g
  71. m.RecordAndExport("src", uint32(v), exp)
  72. }
  73. }(g)
  74. }
  75. wg.Wait()
  76. // We don't assert on the number of exports (it can vary
  77. // based on CAS contention); we assert that the final
  78. // observed max is the actual global max written.
  79. if m.Get("src") != 1000 {
  80. t.Fatalf("final max: want 1000, got %d", m.Get("src"))
  81. }
  82. if exports.Load() < 1 {
  83. t.Fatalf("expected ≥1 export, got %d", exports.Load())
  84. }
  85. }
  86. func TestMaxSeen_NilExportIsNoOp(t *testing.T) {
  87. m := NewMaxSeen()
  88. // Should not panic.
  89. m.RecordAndExport("src-A", 1, nil)
  90. m.RecordAndExport("src-A", 5, nil)
  91. if m.Get("src-A") != 5 {
  92. t.Fatalf("nil export should still update state; got %d", m.Get("src-A"))
  93. }
  94. }