ewma_test.go raw

   1  package ewma
   2  
   3  import (
   4  	"testing"
   5  
   6  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
   7  )
   8  
   9  func TestEWMAConvergence(t *testing.T) {
  10  	ew := NewEWMA(10)
  11  	target := ratio.New(3, 4)
  12  
  13  	// Feed constant value — EWMA should converge to it.
  14  	for range 100 {
  15  		ew.Update(target)
  16  	}
  17  
  18  	diff := ew.Value.Sub(target).Abs()
  19  	epsilon := ratio.New(1, 1000)
  20  	if diff.Greater(epsilon) {
  21  		t.Fatalf("EWMA did not converge: value=%s, target=%s, diff=%s",
  22  			ew.Value, target, diff)
  23  	}
  24  }
  25  
  26  func TestEWMAFirstValue(t *testing.T) {
  27  	ew := NewEWMA(10)
  28  	val := ratio.New(7, 10)
  29  	ew.Update(val)
  30  
  31  	if !ew.Value.Equal(val) {
  32  		t.Fatalf("first update should set value directly: got %s, want %s", ew.Value, val)
  33  	}
  34  }
  35  
  36  func TestOscillationDetected(t *testing.T) {
  37  	d := NewDetector(3, 0, 4) // small window, threshold 4 reversals
  38  
  39  	// Alternate high and low inputs to trigger oscillation.
  40  	detected := false
  41  	for i := range 200 {
  42  		var raw, accreted int64
  43  		if i%2 == 0 {
  44  			raw, accreted = 100, 90 // high ratio
  45  		} else {
  46  			raw, accreted = 100, 10 // low ratio
  47  		}
  48  		if d.Observe(raw, accreted) {
  49  			detected = true
  50  			break
  51  		}
  52  	}
  53  
  54  	if !detected {
  55  		t.Fatalf("oscillation not detected after 200 alternating observations (reversals=%d)",
  56  			d.Reversals)
  57  	}
  58  }
  59  
  60  func TestNoOscillationMonotonic(t *testing.T) {
  61  	d := NewDetector(10, 0, 3)
  62  
  63  	// Monotonically rising input — should never trigger.
  64  	for i := int64(1); i <= 100; i++ {
  65  		if d.Observe(100, i) {
  66  			t.Fatalf("oscillation falsely detected at step %d (reversals=%d)", i, d.Reversals)
  67  		}
  68  	}
  69  }
  70  
  71  func TestResetClearsReversals(t *testing.T) {
  72  	d := NewDetector(3, 0, 4)
  73  
  74  	// Drive to oscillation.
  75  	for i := range 200 {
  76  		if i%2 == 0 {
  77  			d.Observe(100, 90)
  78  		} else {
  79  			d.Observe(100, 10)
  80  		}
  81  		if d.Oscillating() {
  82  			break
  83  		}
  84  	}
  85  
  86  	if !d.Oscillating() {
  87  		t.Fatal("should be oscillating before reset")
  88  	}
  89  
  90  	valueBefore := d.EW.Value
  91  
  92  	d.Reset()
  93  
  94  	if d.Oscillating() {
  95  		t.Fatal("should not be oscillating after reset")
  96  	}
  97  	if d.Reversals != 0 {
  98  		t.Fatalf("reversals not zeroed: %d", d.Reversals)
  99  	}
 100  	if !d.EW.Value.Equal(valueBefore) {
 101  		t.Fatalf("EWMA value changed by reset: was %s, now %s", valueBefore, d.EW.Value)
 102  	}
 103  }
 104  
 105  func TestMarshalUnmarshal(t *testing.T) {
 106  	d := NewDetector(5, 0, 3)
 107  	d.Observe(100, 50)
 108  	d.Observe(100, 60)
 109  	d.Observe(100, 40)
 110  
 111  	data, err := d.Marshal()
 112  	if err != nil {
 113  		t.Fatalf("marshal: %v", err)
 114  	}
 115  
 116  	d2, err := UnmarshalDetector(data)
 117  	if err != nil {
 118  		t.Fatalf("unmarshal: %v", err)
 119  	}
 120  
 121  	if !d2.EW.Value.Equal(d.EW.Value) {
 122  		t.Fatalf("EWMA value mismatch: %s vs %s", d.EW.Value, d2.EW.Value)
 123  	}
 124  	if d2.Threshold != d.Threshold {
 125  		t.Fatalf("threshold mismatch: %d vs %d", d2.Threshold, d.Threshold)
 126  	}
 127  	if d2.EW.Count != d.EW.Count {
 128  		t.Fatalf("count mismatch: %d vs %d", d2.EW.Count, d.EW.Count)
 129  	}
 130  }
 131  
 132  func TestZeroRawCount(t *testing.T) {
 133  	d := NewDetector(5, 0, 3)
 134  	d.Observe(0, 0)
 135  	if !d.EW.Value.Equal(ratio.Zero) {
 136  		t.Fatalf("expected zero value for zero raw, got %s", d.EW.Value)
 137  	}
 138  }
 139  
 140  func TestConvergenceThenOscillation(t *testing.T) {
 141  	d := NewDetector(5, 0, 4)
 142  
 143  	// Phase 1: converge on 50%.
 144  	for range 20 {
 145  		if d.Observe(100, 50) {
 146  			t.Fatal("should not oscillate during convergence")
 147  		}
 148  	}
 149  
 150  	// Phase 2: oscillate around 50%.
 151  	detected := false
 152  	for i := range 200 {
 153  		var accreted int64
 154  		if i%2 == 0 {
 155  			accreted = 60
 156  		} else {
 157  			accreted = 40
 158  		}
 159  		if d.Observe(100, accreted) {
 160  			detected = true
 161  			break
 162  		}
 163  	}
 164  
 165  	if !detected {
 166  		t.Fatalf("oscillation not detected after convergence then alternation (reversals=%d)",
 167  			d.Reversals)
 168  	}
 169  }
 170