mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
243 lines
6.2 KiB
Go
243 lines
6.2 KiB
Go
package core
|
||
|
||
import (
|
||
"sync"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// =============================================================================
|
||
// 单元测试:ScanMetrics 基本操作
|
||
// =============================================================================
|
||
|
||
func TestScanMetrics_Counters(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
|
||
m.RecordConnect(time.Millisecond)
|
||
m.RecordConnect(2 * time.Millisecond)
|
||
m.RecordRefused(500 * time.Microsecond)
|
||
m.RecordTimeout()
|
||
m.RecordExhausted()
|
||
|
||
if m.Total() != 5 {
|
||
t.Errorf("Total() = %d, want 5", m.Total())
|
||
}
|
||
|
||
snap := m.Snapshot()
|
||
if snap.Connects != 2 {
|
||
t.Errorf("Connects = %d, want 2", snap.Connects)
|
||
}
|
||
if snap.Refused != 1 {
|
||
t.Errorf("Refused = %d, want 1", snap.Refused)
|
||
}
|
||
if snap.Timeouts != 1 {
|
||
t.Errorf("Timeouts = %d, want 1", snap.Timeouts)
|
||
}
|
||
if snap.Exhausted != 1 {
|
||
t.Errorf("Exhausted = %d, want 1", snap.Exhausted)
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// 单元测试:RTT EMA 收敛
|
||
// =============================================================================
|
||
|
||
func TestScanMetrics_RTT_EMA(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
|
||
// 喂入稳定的 10ms RTT
|
||
for i := 0; i < 100; i++ {
|
||
m.RecordConnect(10 * time.Millisecond)
|
||
}
|
||
|
||
fast := m.RTTFast()
|
||
if fast < 9*time.Millisecond || fast > 11*time.Millisecond {
|
||
t.Errorf("稳定 10ms 后 RTTFast = %v, 应该接近 10ms", fast)
|
||
}
|
||
|
||
ratio := m.RTTRatio()
|
||
if ratio < 0.9 || ratio > 1.1 {
|
||
t.Errorf("稳定状态 RTTRatio = %.2f, 应该接近 1.0", ratio)
|
||
}
|
||
}
|
||
|
||
func TestScanMetrics_RTT_Trend(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
|
||
// 先喂入 100 个 5ms 建立基线
|
||
for i := 0; i < 100; i++ {
|
||
m.RecordConnect(5 * time.Millisecond)
|
||
}
|
||
|
||
// 再喂入 50 个 50ms(RTT 突增 10 倍)
|
||
for i := 0; i < 50; i++ {
|
||
m.RecordConnect(50 * time.Millisecond)
|
||
}
|
||
|
||
ratio := m.RTTRatio()
|
||
// fast EMA 应该比 slow EMA 高(fast 跟踪快,slow 还没追上来)
|
||
if ratio <= 1.0 {
|
||
t.Errorf("RTT 突增后 RTTRatio = %.2f, 应该 > 1.0", ratio)
|
||
}
|
||
|
||
t.Logf("RTT 突增后: ratio=%.2f, fast=%v", ratio, m.RTTFast())
|
||
}
|
||
|
||
func TestScanMetrics_RTT_InsufficientSamples(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
|
||
// 少于 20 个样本
|
||
for i := 0; i < 10; i++ {
|
||
m.RecordConnect(time.Millisecond)
|
||
}
|
||
|
||
ratio := m.RTTRatio()
|
||
if ratio != 1.0 {
|
||
t.Errorf("样本不足时 RTTRatio = %.2f, 应该是 1.0", ratio)
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// 并发安全测试
|
||
// =============================================================================
|
||
|
||
func TestScanMetrics_ConcurrentSafety(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
var wg sync.WaitGroup
|
||
|
||
for i := 0; i < 100; i++ {
|
||
wg.Add(4)
|
||
go func() { defer wg.Done(); m.RecordConnect(time.Millisecond) }()
|
||
go func() { defer wg.Done(); m.RecordRefused(time.Millisecond) }()
|
||
go func() { defer wg.Done(); m.RecordTimeout() }()
|
||
go func() { defer wg.Done(); m.RecordExhausted() }()
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
if m.Total() != 400 {
|
||
t.Errorf("并发后 Total() = %d, want 400", m.Total())
|
||
}
|
||
|
||
// 验证 Snapshot 不 panic
|
||
snap := m.Snapshot()
|
||
if snap.Total() != 400 {
|
||
t.Errorf("并发后 Snapshot.Total() = %d, want 400", snap.Total())
|
||
}
|
||
|
||
// 验证 RTTRatio 不 panic
|
||
_ = m.RTTRatio()
|
||
}
|
||
|
||
// =============================================================================
|
||
// 补充测试:按题目要求的函数名
|
||
// =============================================================================
|
||
|
||
// TestScanMetricsTotal — 各计数器各调一次,Total() 应返回 4
|
||
func TestScanMetricsTotal(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
m.RecordConnect(time.Millisecond)
|
||
m.RecordRefused(time.Millisecond)
|
||
m.RecordTimeout()
|
||
m.RecordExhausted()
|
||
if got := m.Total(); got != 4 {
|
||
t.Errorf("Total() = %d, want 4", got)
|
||
}
|
||
}
|
||
|
||
// TestScanMetricsSnapshot — 记录数据后 Snapshot() 返回正确快照
|
||
func TestScanMetricsSnapshot(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
m.RecordConnect(5 * time.Millisecond)
|
||
m.RecordConnect(10 * time.Millisecond)
|
||
m.RecordRefused(2 * time.Millisecond)
|
||
m.RecordTimeout()
|
||
m.RecordExhausted()
|
||
|
||
snap := m.Snapshot()
|
||
tests := []struct {
|
||
name string
|
||
got int64
|
||
want int64
|
||
}{
|
||
{"Connects", snap.Connects, 2},
|
||
{"Refused", snap.Refused, 1},
|
||
{"Timeouts", snap.Timeouts, 1},
|
||
{"Exhausted", snap.Exhausted, 1},
|
||
}
|
||
for _, tt := range tests {
|
||
if tt.got != tt.want {
|
||
t.Errorf("Snapshot.%s = %d, want %d", tt.name, tt.got, tt.want)
|
||
}
|
||
}
|
||
if snap.RTTFastNs <= 0 {
|
||
t.Errorf("Snapshot.RTTFastNs = %d, want > 0", snap.RTTFastNs)
|
||
}
|
||
}
|
||
|
||
// TestScanMetricsRTTRatio — 样本不足返回 1.0;20+ 个相同 RTT 接近 1.0
|
||
func TestScanMetricsRTTRatio(t *testing.T) {
|
||
t.Run("样本不足返回1.0", func(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
for i := 0; i < 19; i++ {
|
||
m.RecordConnect(time.Millisecond)
|
||
}
|
||
if r := m.RTTRatio(); r != 1.0 {
|
||
t.Errorf("样本不足 RTTRatio() = %f, want 1.0", r)
|
||
}
|
||
})
|
||
|
||
t.Run("稳定RTT接近1.0", func(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
for i := 0; i < 30; i++ {
|
||
m.RecordConnect(10 * time.Millisecond)
|
||
}
|
||
r := m.RTTRatio()
|
||
if r < 0.9 || r > 1.1 {
|
||
t.Errorf("稳定RTT下 RTTRatio() = %f, want ~1.0", r)
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestScanMetricsRTTFast — 初始为 0,记录后非零
|
||
func TestScanMetricsRTTFast(t *testing.T) {
|
||
m := &ScanMetrics{}
|
||
if m.RTTFast() != 0 {
|
||
t.Errorf("初始 RTTFast() = %v, want 0", m.RTTFast())
|
||
}
|
||
m.RecordConnect(5 * time.Millisecond)
|
||
if m.RTTFast() == 0 {
|
||
t.Errorf("记录后 RTTFast() 仍为 0")
|
||
}
|
||
}
|
||
|
||
// TestMetricsSnapshotTotal — MetricsSnapshot 各字段求和
|
||
func TestMetricsSnapshotTotal(t *testing.T) {
|
||
snap := MetricsSnapshot{Connects: 1, Refused: 2, Timeouts: 3, Exhausted: 4}
|
||
if got := snap.Total(); got != 10 {
|
||
t.Errorf("MetricsSnapshot.Total() = %d, want 10", got)
|
||
}
|
||
}
|
||
|
||
// TestUpdateEMA — 直接测 updateEMA 行为
|
||
func TestUpdateEMA(t *testing.T) {
|
||
t.Run("target为0时直接设为sample", func(t *testing.T) {
|
||
var a atomic.Int64
|
||
updateEMA(&a, 100, 10)
|
||
if got := a.Load(); got != 100 {
|
||
t.Errorf("初始为0时 updateEMA 结果 = %d, want 100", got)
|
||
}
|
||
})
|
||
|
||
t.Run("target非零时做EMA更新", func(t *testing.T) {
|
||
var a atomic.Int64
|
||
a.Store(200)
|
||
// next = 200 + (100-200)/10 = 200 - 10 = 190
|
||
updateEMA(&a, 100, 10)
|
||
if got := a.Load(); got != 190 {
|
||
t.Errorf("EMA更新结果 = %d, want 190", got)
|
||
}
|
||
})
|
||
}
|