blockchain/internal/lib/strategy/sma_test.go

121 lines
2.7 KiB
Go
Raw Normal View History

2025-08-15 01:36:36 +00:00
package strategy
import (
2025-08-17 14:48:13 +00:00
"sync"
2025-08-15 01:36:36 +00:00
"testing"
2025-08-17 14:48:13 +00:00
"time"
2025-08-15 01:36:36 +00:00
2025-08-17 14:48:13 +00:00
"github.com/shopspring/decimal"
)
2025-08-15 01:36:36 +00:00
2025-08-17 14:48:13 +00:00
func d(i int64) decimal.Decimal { return decimal.NewFromInt(i) }
2025-08-15 01:36:36 +00:00
2025-08-17 14:48:13 +00:00
func TestSMA_WarmupAndSliding(t *testing.T) {
type step struct {
in int64
wantVal string // 用字串比對可避免浮點誤差decimal 本就精準)
ready bool
2025-08-15 01:36:36 +00:00
}
2025-08-17 14:48:13 +00:00
tests := []struct {
name string
window uint
steps []step
2025-08-15 01:36:36 +00:00
}{
{
2025-08-17 14:48:13 +00:00
name: "warmup_then_ready_and_slide",
window: 3,
steps: []step{
{in: 1, wantVal: "1", ready: false}, // [1] avg=1
{in: 2, wantVal: "1.5", ready: false}, // [1,2] avg=1.5
{in: 3, wantVal: "2", ready: true}, // [1,2,3] avg=2
{in: 4, wantVal: "3", ready: true}, // [2,3,4] avg=3
{in: 5, wantVal: "4", ready: true}, // [3,4,5] avg=4
{in: 6, wantVal: "5", ready: true}, // [4,5,6] avg=5
{in: 7, wantVal: "6", ready: true}, // [5,6,7] avg=6
2025-08-15 01:36:36 +00:00
},
},
{
2025-08-17 14:48:13 +00:00
name: "window_1_behaves_as_latest_value",
window: 1,
steps: []step{
{in: 10, wantVal: "10", ready: true},
{in: 11, wantVal: "11", ready: true},
{in: 12, wantVal: "12", ready: true},
2025-08-15 01:36:36 +00:00
},
},
}
2025-08-17 14:48:13 +00:00
for _, tc := range tests {
tc := tc
2025-08-15 01:36:36 +00:00
t.Run(tc.name, func(t *testing.T) {
2025-08-17 14:48:13 +00:00
s := NewSMA(tc.window)
for i, st := range tc.steps {
got := s.Update(d(st.in))
if got.Value.String() != st.wantVal {
t.Fatalf("step %d: got value %s, want %s", i, got.Value, st.wantVal)
}
if got.Ready != st.ready {
t.Fatalf("step %d: ready mismatch, got %v, want %v", i, got.Ready, st.ready)
}
// Load() 應該等於最新快照
ld := s.Load()
if ld.Value.String() != st.wantVal || ld.Ready != st.ready {
t.Fatalf("step %d: Load() not latest snapshot", i)
2025-08-15 01:36:36 +00:00
}
}
2025-08-17 14:48:13 +00:00
})
}
}
2025-08-15 01:36:36 +00:00
2025-08-17 14:48:13 +00:00
func TestSMA_NewZeroWindowShouldPanic(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic when window=0")
}
}()
_ = NewSMA(0)
}
func TestSMA_MultiReadersSingleWriter(t *testing.T) {
s := NewSMA(3)
var wg sync.WaitGroup
stop := make(chan struct{})
// 多 reader 併發讀取(零鎖)
reader := func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = s.Load() // 不做判斷,重點是不得 panic
2025-08-15 01:36:36 +00:00
}
2025-08-17 14:48:13 +00:00
}
}
// 啟動多個讀者
for i := 0; i < 8; i++ {
wg.Add(1)
go reader()
}
// 單 writer 更新
prices := []int64{1, 2, 3, 4, 5, 6, 7}
for _, p := range prices {
s.Update(d(p))
time.Sleep(1 * time.Millisecond)
}
// 停 reader
close(stop)
wg.Wait()
// 最終應為 [5,6,7] 的平均=6且 ready=true
got := s.Load()
if got.Value.String() != "6" || !got.Ready {
t.Fatalf("final snapshot mismatch: got value=%s ready=%v", got.Value, got.Ready)
2025-08-15 01:36:36 +00:00
}
}