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