Files
fscan/core/edge_cases_test.go
T
ZacharyZcR 45ebe7040e
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
优化自适应扫描系统 & 修复 POC 调度问题
自适应扫描优化:
- target/ceiling 分离,自适应池可向上探索而非锁死在 target
- assessHealth 阈值按网络环境区分(LAN 收紧 / Internet 放宽)
- RTT 漂移时动态压低 target,配合 AIMD 双重降速
- 去掉 semaphore 双层流控,由 ants pool 统一反压
- 探测端口从 3 个扩充到 8 个,减少 RTT 采样偏差
- computeRetries 按环境调整目标概率和上限

Bug 修复:
- AdaptivePool.Wait() 加 10 分钟超时,防止 goroutine 卡死时永久挂起
- CEL 环境初始化失败后允许重试(sync.Once → sync.Mutex + 标志位)
- CAS 自旋加 runtime.Gosched() 退避,减少高并发下 CPU 空转
- -full 模式下 web 插件跳过 IsMarkedWebService 检查 #588
- 不确定服务补做 HTTP 回退探测,覆盖自定义框架漏网场景
- POC sets 纯字面量值跳过 CEL 编译,消除大量误报错误日志
2026-06-13 12:39:24 +08:00

652 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"math"
"sync"
"testing"
"time"
)
// =============================================================================
// computeRetries 边界
// =============================================================================
func TestComputeRetries_EdgeCases(t *testing.T) {
tests := []struct {
lossRate float64
wantMin int
wantMax int
desc string
}{
{-0.5, 1, 1, "负数丢包率: 视为零"},
{-1.0, 1, 1, "负一: 视为零"},
{0.0, 1, 1, "精确零"},
{0.001, 1, 1, "精确边界 0.001"},
{0.0009, 1, 1, "低于 0.001 边界"},
{0.0011, 1, 5, "高于 0.001 边界"},
{0.95, 5, 5, "精确边界 0.95"},
{0.949, 1, 5, "低于 0.95 边界"},
{0.951, 5, 5, "高于 0.95 边界"},
{1.0, 5, 5, "精确 1.0"},
{1.5, 5, 5, "超过 1.0"},
{100.0, 5, 5, "极大值"},
{math.SmallestNonzeroFloat64, 1, 1, "最小正浮点数"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := computeRetries(tt.lossRate, EnvWAN)
if got < tt.wantMin || got > tt.wantMax {
t.Errorf("computeRetries(%v) = %d, want [%d, %d]",
tt.lossRate, got, tt.wantMin, tt.wantMax)
}
if got < 1 || got > 5 {
t.Errorf("computeRetries(%v) = %d, 超出 [1,6] 范围", tt.lossRate, got)
}
})
}
}
func TestComputeRetries_NaN_Inf(t *testing.T) {
// 确保不 panic
for _, v := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
got := computeRetries(v, EnvWAN)
if got < 1 || got > 5 {
t.Errorf("computeRetries(%v) = %d, 超出 [1,6] 范围", v, got)
}
}
}
// =============================================================================
// computeICMPRate 边界
// =============================================================================
func TestComputeICMPRate_EdgeCases(t *testing.T) {
tests := []struct {
env NetworkEnv
fdLimit int
desc string
}{
{EnvLAN, 1, "fd=1: 极小"},
{EnvLAN, -1, "fd=负数: 应被忽略"},
{EnvLAN, 0, "fd=0: 未知"},
{EnvLAN, math.MaxInt32, "fd=极大"},
{NetworkEnv(99), 1024, "未知环境类型"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
net := &NetworkProfile{Env: tt.env}
sys := &SystemProfile{FDLimit: tt.fdLimit}
got := computeICMPRate(net, sys)
if got <= 0 || math.IsNaN(got) || math.IsInf(got, 0) {
t.Errorf("computeICMPRate(env=%v, fd=%d) = %v, 无效值", tt.env, tt.fdLimit, got)
}
})
}
}
// =============================================================================
// classifyEnv 精确边界值
// =============================================================================
func TestClassifyEnv_ExactBoundaries(t *testing.T) {
tests := []struct {
median time.Duration
lossRate float64
want NetworkEnv
desc string
}{
// RTT 边界
{4999 * time.Microsecond, 0.0, EnvLAN, "4.999ms → LAN"},
{5 * time.Millisecond, 0.0, EnvWAN, "精确 5ms → WAN"},
{49999 * time.Microsecond, 0.0, EnvWAN, "49.999ms → WAN"},
{50 * time.Millisecond, 0.0, EnvInternet, "精确 50ms → Internet"},
{199999 * time.Microsecond, 0.0, EnvInternet, "199.999ms → Internet"},
{200 * time.Millisecond, 0.0, EnvSlow, "精确 200ms → Slow"},
// 丢包率边界
{1 * time.Millisecond, 0.009, EnvLAN, "丢包 0.9% → LAN"},
{1 * time.Millisecond, 0.01, EnvWAN, "精确 1% → WAN (不满足 < 0.01)"},
{1 * time.Millisecond, 0.011, EnvWAN, "丢包 1.1% → WAN (超过 LAN 阈值)"},
{20 * time.Millisecond, 0.049, EnvWAN, "丢包 4.9% → WAN"},
{20 * time.Millisecond, 0.05, EnvInternet, "精确 5% → Internet (不满足 < 0.05)"},
{20 * time.Millisecond, 0.051, EnvInternet, "丢包 5.1% → Internet"},
{1 * time.Millisecond, 0.099, EnvInternet, "丢包 9.9% → Internet"},
{1 * time.Millisecond, 0.10, EnvInternet, "精确 10% → Internet (< 判断)"},
{1 * time.Millisecond, 0.101, EnvSlow, "丢包 10.1% → Slow"},
// 零值
{0, 0.0, EnvLAN, "零 RTT 零丢包 → LAN"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := classifyEnv(tt.median, tt.lossRate)
if got != tt.want {
t.Errorf("classifyEnv(median=%v, loss=%.4f) = %v, want %v",
tt.median, tt.lossRate, got, tt.want)
}
})
}
}
// =============================================================================
// classifyNetwork 边界
// =============================================================================
func TestClassifyNetwork_EdgeCases(t *testing.T) {
t.Run("单个 RTT 样本", func(t *testing.T) {
p := classifyNetwork([]time.Duration{5 * time.Millisecond}, 0, 1)
if p.Samples != 1 {
t.Errorf("samples = %d, want 1", p.Samples)
}
// stddev 应该是 0
if p.RTTStddev != 0 {
t.Errorf("单样本 stddev = %v, want 0", p.RTTStddev)
}
})
t.Run("所有 RTT 相同", func(t *testing.T) {
rtts := make([]time.Duration, 50)
for i := range rtts {
rtts[i] = 10 * time.Millisecond
}
p := classifyNetwork(rtts, 0, 50)
if p.RTTStddev != 0 {
t.Errorf("全相同 RTT stddev = %v, want 0", p.RTTStddev)
}
if p.RTTMedian != 10*time.Millisecond {
t.Errorf("median = %v, want 10ms", p.RTTMedian)
}
})
t.Run("极大 RTT 值", func(t *testing.T) {
rtts := []time.Duration{time.Hour, time.Hour, time.Hour}
p := classifyNetwork(rtts, 0, 3)
if p.Env != EnvSlow {
t.Errorf("env = %v, want Slow", p.Env)
}
})
t.Run("混合极端值", func(t *testing.T) {
rtts := []time.Duration{time.Microsecond, time.Hour}
p := classifyNetwork(rtts, 0, 2)
// 不 panic 就行
if p.Samples != 2 {
t.Errorf("samples = %d, want 2", p.Samples)
}
})
t.Run("全部失败无响应", func(t *testing.T) {
p := classifyNetwork(nil, 100, 100)
if p.Env != EnvWAN {
t.Errorf("env = %v, want WAN (default)", p.Env)
}
})
t.Run("failures > total (异常输入)", func(t *testing.T) {
rtts := []time.Duration{time.Millisecond}
p := classifyNetwork(rtts, 10, 5) // failures > total
// lossRate = 1 - 1/5 = 0.8, 不应 panic
if p.LossRate < 0 {
t.Errorf("lossRate = %.2f, 不应为负", p.LossRate)
}
})
t.Run("total=0", func(t *testing.T) {
p := classifyNetwork(nil, 0, 0)
// 不 panic
if p.Samples != 0 {
t.Errorf("samples = %d, want 0", p.Samples)
}
})
}
// =============================================================================
// RecommendConcurrency 边界
// =============================================================================
func TestRecommendConcurrency_EdgeCases(t *testing.T) {
tests := []struct {
env NetworkEnv
loss float64
userT int
explicit bool
desc string
}{
{EnvLAN, 0.0, 0, false, "userThreadNum=0"},
{EnvLAN, 0.0, 1, false, "userThreadNum=1"},
{EnvLAN, 0.0, -1, false, "userThreadNum 负数"},
{EnvLAN, 0.0, math.MaxInt32, false, "userThreadNum 极大"},
{EnvLAN, 0.99, 600, false, "99% 丢包"},
{EnvLAN, 1.0, 600, false, "100% 丢包"},
{EnvSlow, 0.0, 1, true, "慢速+显式+1"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
p := &NetworkProfile{Env: tt.env, LossRate: tt.loss, Samples: 10}
target, ceiling := p.RecommendConcurrency(tt.userT, tt.explicit)
// 不 panic,且 target >= 1clamp 保底 10 或 userT
if target < 0 || ceiling < 0 {
t.Errorf("target=%d ceiling=%d, 不应为负", target, ceiling)
}
if tt.explicit && ceiling != tt.userT && tt.userT > 0 {
t.Errorf("显式模式 ceiling=%d, want %d", ceiling, tt.userT)
}
t.Logf("env=%v loss=%.2f userT=%d explicit=%v → target=%d ceiling=%d",
tt.env, tt.loss, tt.userT, tt.explicit, target, ceiling)
})
}
}
// =============================================================================
// ScanMetrics 边界
// =============================================================================
func TestScanMetrics_EdgeCases(t *testing.T) {
t.Run("RTT=0", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(0)
// 不 panic
if m.Total() != 1 {
t.Errorf("Total = %d, want 1", m.Total())
}
})
t.Run("负数 RTT", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(-time.Millisecond)
// 不 panic,负数 RTT 应被忽略
if m.rttSamples.Load() != 0 {
t.Errorf("负数 RTT 不应计入采样: got %d", m.rttSamples.Load())
}
})
t.Run("极大 RTT", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(time.Hour)
if m.RTTFast() != time.Hour {
t.Errorf("首个样本 RTTFast = %v, want 1h", m.RTTFast())
}
})
t.Run("EMA 首个样本初始化", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(10 * time.Millisecond)
if m.rttFastNs.Load() != int64(10*time.Millisecond) {
t.Errorf("首个样本应直接设置 EMA: got %d", m.rttFastNs.Load())
}
})
t.Run("空 Snapshot", func(t *testing.T) {
m := &ScanMetrics{}
snap := m.Snapshot()
if snap.Total() != 0 {
t.Errorf("空 metrics Snapshot.Total = %d, want 0", snap.Total())
}
})
t.Run("RTTRatio 单侧为零", func(t *testing.T) {
m := &ScanMetrics{}
// 手动设置一个但不设另一个——不应该发生,但防御
m.rttFastNs.Store(1000)
m.rttSlowNs.Store(0)
m.rttSamples.Store(30)
ratio := m.RTTRatio()
if ratio != 1.0 {
t.Errorf("slow=0 时 ratio = %.2f, want 1.0", ratio)
}
})
t.Run("大量操作不溢出", func(t *testing.T) {
m := &ScanMetrics{}
for i := 0; i < 100000; i++ {
m.RecordConnect(time.Millisecond)
}
if m.Total() != 100000 {
t.Errorf("Total = %d, want 100000", m.Total())
}
ratio := m.RTTRatio()
if math.IsNaN(ratio) || math.IsInf(ratio, 0) {
t.Errorf("大量样本后 ratio = %v, 不应为 NaN/Inf", ratio)
}
})
}
// =============================================================================
// TuneConfig 边界
// =============================================================================
func TestTuneConfig_EdgeCases(t *testing.T) {
t.Run("RTTMedian=0 RTTStddev=0", func(t *testing.T) {
config := makeDefaultConfig()
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvLAN, RTTMedian: 0, RTTStddev: 0, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
// Timeout: median(0) + 4*stddev(0) = 0 → minTO = 0+200ms → clamp to 1s
if config.Timeout < time.Second {
t.Errorf("零 RTT Timeout = %v, 应该 >= 1s", config.Timeout)
}
})
t.Run("RTTStddev 远大于 RTTMedian", func(t *testing.T) {
config := makeDefaultConfig()
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvInternet, RTTMedian: 10 * time.Millisecond, RTTStddev: 5 * time.Second, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
// Timeout = 10ms + 4*5s = 20.01s → clamp to 10s
if config.Timeout != 10*time.Second {
t.Errorf("极大 stddev Timeout = %v, 应该被 clamp 到 10s", config.Timeout)
}
})
t.Run("ThreadNum=0", func(t *testing.T) {
config := makeDefaultConfig()
config.ThreadNum = 0
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvLAN, RTTMedian: time.Millisecond, RTTStddev: time.Millisecond, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
// ModuleThreadNum = 0/30 = 0 → clamp to 5
if config.ModuleThreadNum < 5 {
t.Errorf("ThreadNum=0 时 ModuleThreadNum = %d, 应该 >= 5", config.ModuleThreadNum)
}
})
t.Run("多次调用 TuneConfig", func(t *testing.T) {
config := makeDefaultConfig()
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvLAN, RTTMedian: time.Millisecond, RTTStddev: time.Millisecond, LossRate: 0.0, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
first := config.Timeout
// 第二次调用——已经调整过的值不等于默认值,应被视为"显式"
ep.TuneConfig(config, session)
second := config.Timeout
if first != second {
t.Errorf("多次调用 TuneConfig 不应重复调整: %v vs %v", first, second)
}
})
t.Run("fd limit = ThreadNum 精确值", func(t *testing.T) {
config := makeDefaultConfig()
config.ThreadNum = 600
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Samples: 0},
System: SystemProfile{FDLimit: 1000}, // 1000 * 0.6 = 600
}
ep.TuneConfig(config, session)
// ThreadNum(600) == maxConcurrency(600), 不应触发约束
if config.ThreadNum != 600 {
t.Errorf("fd=1000 时 ThreadNum = %d, 不应被约束", config.ThreadNum)
}
})
t.Run("fd limit 精确低于 ThreadNum", func(t *testing.T) {
config := makeDefaultConfig()
config.ThreadNum = 600
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Samples: 0},
System: SystemProfile{FDLimit: 999}, // 999 * 0.6 = 599
}
ep.TuneConfig(config, session)
if config.ThreadNum > 599 {
t.Errorf("fd=999 时 ThreadNum = %d, 应该 <= 599", config.ThreadNum)
}
})
}
// =============================================================================
// AdaptivePool 边界
// =============================================================================
func TestAdaptivePool_EdgeCases(t *testing.T) {
t.Run("target=1", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(1, 1, func(interface{}) {}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
// initial = max(1/4, 10) = 10 → 但 10 > target(1)... 看实现
// 实际上 initial = min(max(1/4, 10), 1) = 1... 不对
// initial = target/4 = 0, 但 < 10, 所以 initial = 10
// 但 initial > target(1)... initial = min(10, 1) = 1
// 看代码:if initial > target { initial = target }
if pool.Cap() != 1 {
t.Errorf("target=1 时 cap = %d, want 1", pool.Cap())
}
})
t.Run("target=0", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(0, 0, func(interface{}) {}, metrics)
// ants 可能拒绝 size=0
if err != nil {
t.Logf("target=0 正确返回错误: %v", err)
return
}
defer pool.Release()
t.Logf("target=0 cap = %d", pool.Cap())
})
t.Run("ceiling < target", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(100, 50, func(interface{}) {}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
// initial = 100/4 = 25, 不超过 ceiling
if pool.Cap() > 50 {
t.Errorf("ceiling=50 但 cap = %d", pool.Cap())
}
})
t.Run("高频 Invoke 不 panic", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(10, 10, func(interface{}) {
time.Sleep(time.Millisecond)
}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
pool.inSlowStart = false
pool.tune(10)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = pool.Invoke(nil)
}()
}
wg.Wait()
pool.Wait()
})
t.Run("assessHealth 零增量", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(100, 100, func(interface{}) {}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
// 初始化 prevSnapshot 后不产生新数据
pool.prevSnapshot = metrics.Snapshot()
health := pool.assessHealth()
if health != HealthUnknown {
t.Errorf("零增量应返回 HealthUnknown, got %v", health)
}
})
}
// =============================================================================
// pickSamples 边界
// =============================================================================
func TestPickSamples_EdgeCases(t *testing.T) {
t.Run("maxSamples=0", func(t *testing.T) {
s := pickSamples([]string{"a", "b"}, 0)
if len(s) != 0 {
t.Errorf("maxSamples=0 应返回空, got %d", len(s))
}
})
t.Run("maxSamples=1", func(t *testing.T) {
s := pickSamples([]string{"a", "b", "c"}, 1)
if len(s) != 1 {
t.Errorf("maxSamples=1 应返回 1 个, got %d", len(s))
}
})
t.Run("hosts 等于 maxSamples", func(t *testing.T) {
hosts := []string{"a", "b", "c"}
s := pickSamples(hosts, 3)
if len(s) != 3 {
t.Errorf("应返回全部, got %d", len(s))
}
})
}
// =============================================================================
// isTimeoutError / isConnectionRefused 边界
// =============================================================================
func TestIsTimeoutError_EdgeCases(t *testing.T) {
if isTimeoutError(nil) {
t.Error("nil 不应判为 timeout")
}
}
func TestIsConnectionRefused_EdgeCases(t *testing.T) {
if isConnectionRefused(nil) {
t.Error("nil 不应判为 refused")
}
}
// =============================================================================
// NetworkEnv.String 覆盖
// =============================================================================
func TestNetworkEnv_String(t *testing.T) {
for _, env := range []NetworkEnv{EnvLAN, EnvWAN, EnvInternet, EnvSlow} {
s := env.String()
if s == "" {
t.Errorf("NetworkEnv(%d).String() = 空", env)
}
}
// 未知值
s := NetworkEnv(99).String()
if s == "" {
t.Error("未知 NetworkEnv.String() = 空")
}
}
// =============================================================================
// clampInt / clampDuration 边界
// =============================================================================
func TestClampInt(t *testing.T) {
tests := []struct {
v, min, max, want int
}{
{5, 1, 10, 5},
{0, 1, 10, 1},
{15, 1, 10, 10},
{-5, -10, -1, -5},
{5, 5, 5, 5}, // min == max == v
{3, 5, 5, 5}, // v < min == max
{10, 5, 5, 5}, // v > min == max
}
for _, tt := range tests {
got := clampInt(tt.v, tt.min, tt.max)
if got != tt.want {
t.Errorf("clampInt(%d, %d, %d) = %d, want %d", tt.v, tt.min, tt.max, got, tt.want)
}
}
}
func TestClampDuration(t *testing.T) {
got := clampDuration(5*time.Second, time.Second, 10*time.Second)
if got != 5*time.Second {
t.Errorf("got %v, want 5s", got)
}
got = clampDuration(0, time.Second, 10*time.Second)
if got != time.Second {
t.Errorf("got %v, want 1s", got)
}
got = clampDuration(time.Hour, time.Second, 10*time.Second)
if got != 10*time.Second {
t.Errorf("got %v, want 10s", got)
}
}
// =============================================================================
// isExplicit 边界
// =============================================================================
func TestIsExplicit(t *testing.T) {
config := makeDefaultConfig()
// 默认值 → 非显式
if isExplicit(config, "time") {
t.Error("默认 Timeout 不应视为显式")
}
if isExplicit(config, "mt") {
t.Error("默认 ModuleThreadNum 不应视为显式")
}
if isExplicit(config, "retry") {
t.Error("默认 MaxRetries 不应视为显式")
}
if isExplicit(config, "icmp-rate") {
t.Error("默认 ICMPRate 不应视为显式")
}
if isExplicit(config, "num") {
t.Error("默认 PocNum 不应视为显式")
}
// 未知 flag
if isExplicit(config, "nonexistent") {
t.Error("未知 flag 不应视为显式")
}
// ThreadNumExplicit
config.ThreadNumExplicit = true
if !isExplicit(config, "t") {
t.Error("ThreadNumExplicit=true 应视为显式")
}
config = makeDefaultConfig()
config.TimeoutExplicit = true
config.ModuleThreadNumExplicit = true
config.MaxRetriesExplicit = true
config.Network.ICMPRateExplicit = true
config.POC.NumExplicit = true
if !isExplicit(config, "time") || !isExplicit(config, "mt") ||
!isExplicit(config, "retry") || !isExplicit(config, "icmp-rate") ||
!isExplicit(config, "num") {
t.Error("显式标记为 true 时默认值也应视为显式")
}
}