test(backend): 补充改动代码单元测试覆盖率至 85%+
新增 48 个测试用例覆盖修复代码的各分支路径: - subscription_maintenance_queue: nil receiver/task、Stop 幂等、零值参数 (+6) - billing_service: CalculateCostWithConfig、错误传播、SoraImageCost 等 (+12) - timing_wheel_service: Schedule/ScheduleRecurring after Stop (+3) - sora_media_cleanup_service: nil guard、Start/Stop 各分支、timezone (+10) - sora_gateway_service: normalizeSoraMediaURLs、buildSoraContent 等辅助函数 (+17) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -292,6 +292,133 @@ func TestCalculateCost_ZeroTokens(t *testing.T) {
|
||||
require.Equal(t, 0.0, cost.ActualCost)
|
||||
}
|
||||
|
||||
func TestCalculateCostWithConfig(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.Default.RateMultiplier = 1.5
|
||||
svc := NewBillingService(cfg, nil)
|
||||
|
||||
tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500}
|
||||
cost, err := svc.CalculateCostWithConfig("claude-sonnet-4", tokens)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := svc.CalculateCost("claude-sonnet-4", tokens, 1.5)
|
||||
require.InDelta(t, expected.ActualCost, cost.ActualCost, 1e-10)
|
||||
}
|
||||
|
||||
func TestCalculateCostWithConfig_ZeroMultiplier(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.Default.RateMultiplier = 0
|
||||
svc := NewBillingService(cfg, nil)
|
||||
|
||||
tokens := UsageTokens{InputTokens: 1000}
|
||||
cost, err := svc.CalculateCostWithConfig("claude-sonnet-4", tokens)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 倍率 <=0 时默认 1.0
|
||||
expected, _ := svc.CalculateCost("claude-sonnet-4", tokens, 1.0)
|
||||
require.InDelta(t, expected.ActualCost, cost.ActualCost, 1e-10)
|
||||
}
|
||||
|
||||
func TestGetEstimatedCost(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
est, err := svc.GetEstimatedCost("claude-sonnet-4", 1000, 500)
|
||||
require.NoError(t, err)
|
||||
require.True(t, est > 0)
|
||||
}
|
||||
|
||||
func TestListSupportedModels(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
models := svc.ListSupportedModels()
|
||||
require.NotEmpty(t, models)
|
||||
require.GreaterOrEqual(t, len(models), 6)
|
||||
}
|
||||
|
||||
func TestGetPricingServiceStatus_NilService(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
status := svc.GetPricingServiceStatus()
|
||||
require.NotNil(t, status)
|
||||
require.Equal(t, "using fallback", status["last_updated"])
|
||||
}
|
||||
|
||||
func TestForceUpdatePricing_NilService(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
err := svc.ForceUpdatePricing()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "not initialized")
|
||||
}
|
||||
|
||||
func TestCalculateSoraImageCost(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
price360 := 0.05
|
||||
price540 := 0.08
|
||||
cfg := &SoraPriceConfig{ImagePrice360: &price360, ImagePrice540: &price540}
|
||||
|
||||
cost := svc.CalculateSoraImageCost("360", 2, cfg, 1.0)
|
||||
require.InDelta(t, 0.10, cost.TotalCost, 1e-10)
|
||||
|
||||
cost540 := svc.CalculateSoraImageCost("540", 1, cfg, 2.0)
|
||||
require.InDelta(t, 0.08, cost540.TotalCost, 1e-10)
|
||||
require.InDelta(t, 0.16, cost540.ActualCost, 1e-10)
|
||||
}
|
||||
|
||||
func TestCalculateSoraImageCost_ZeroCount(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
cost := svc.CalculateSoraImageCost("360", 0, nil, 1.0)
|
||||
require.Equal(t, 0.0, cost.TotalCost)
|
||||
}
|
||||
|
||||
func TestCalculateSoraVideoCost_NilConfig(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
cost := svc.CalculateSoraVideoCost("sora-video", nil, 1.0)
|
||||
require.Equal(t, 0.0, cost.TotalCost)
|
||||
}
|
||||
|
||||
func TestCalculateCostWithLongContext_PropagatesError(t *testing.T) {
|
||||
// 使用空的 fallback prices 让 GetModelPricing 失败
|
||||
svc := &BillingService{
|
||||
cfg: &config.Config{},
|
||||
fallbackPrices: make(map[string]*ModelPricing),
|
||||
}
|
||||
|
||||
tokens := UsageTokens{InputTokens: 300000, CacheReadTokens: 0}
|
||||
_, err := svc.CalculateCostWithLongContext("unknown-model", tokens, 1.0, 200000, 2.0)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "pricing not found")
|
||||
}
|
||||
|
||||
func TestCalculateCost_SupportsCacheBreakdown(t *testing.T) {
|
||||
svc := &BillingService{
|
||||
cfg: &config.Config{},
|
||||
fallbackPrices: map[string]*ModelPricing{
|
||||
"claude-sonnet-4": {
|
||||
InputPricePerToken: 3e-6,
|
||||
OutputPricePerToken: 15e-6,
|
||||
SupportsCacheBreakdown: true,
|
||||
CacheCreation5mPrice: 4.0, // per million tokens
|
||||
CacheCreation1hPrice: 5.0, // per million tokens
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tokens := UsageTokens{
|
||||
InputTokens: 1000,
|
||||
OutputTokens: 500,
|
||||
CacheCreation5mTokens: 100000,
|
||||
CacheCreation1hTokens: 50000,
|
||||
}
|
||||
cost, err := svc.CalculateCost("claude-sonnet-4", tokens, 1.0)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected5m := float64(100000) / 1_000_000 * 4.0
|
||||
expected1h := float64(50000) / 1_000_000 * 5.0
|
||||
require.InDelta(t, expected5m+expected1h, cost.CacheCreationCost, 1e-10)
|
||||
}
|
||||
|
||||
func TestCalculateCost_LargeTokenCount(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -100,6 +101,150 @@ func TestSoraGatewayService_BuildSoraMediaURLSigned(t *testing.T) {
|
||||
require.Contains(t, url, "sig=")
|
||||
}
|
||||
|
||||
func TestNormalizeSoraMediaURLs_Empty(t *testing.T) {
|
||||
svc := NewSoraGatewayService(nil, nil, nil, &config.Config{})
|
||||
result := svc.normalizeSoraMediaURLs(nil)
|
||||
require.Empty(t, result)
|
||||
|
||||
result = svc.normalizeSoraMediaURLs([]string{})
|
||||
require.Empty(t, result)
|
||||
}
|
||||
|
||||
func TestNormalizeSoraMediaURLs_HTTPUrls(t *testing.T) {
|
||||
svc := NewSoraGatewayService(nil, nil, nil, &config.Config{})
|
||||
urls := []string{"https://example.com/a.png", "http://example.com/b.mp4"}
|
||||
result := svc.normalizeSoraMediaURLs(urls)
|
||||
require.Equal(t, urls, result)
|
||||
}
|
||||
|
||||
func TestNormalizeSoraMediaURLs_LocalPaths(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
svc := NewSoraGatewayService(nil, nil, nil, cfg)
|
||||
urls := []string{"/image/2025/01/a.png", "video/2025/01/b.mp4"}
|
||||
result := svc.normalizeSoraMediaURLs(urls)
|
||||
require.Len(t, result, 2)
|
||||
require.Contains(t, result[0], "/sora/media")
|
||||
require.Contains(t, result[1], "/sora/media")
|
||||
}
|
||||
|
||||
func TestNormalizeSoraMediaURLs_SkipsBlank(t *testing.T) {
|
||||
svc := NewSoraGatewayService(nil, nil, nil, &config.Config{})
|
||||
urls := []string{"https://example.com/a.png", "", " ", "https://example.com/b.png"}
|
||||
result := svc.normalizeSoraMediaURLs(urls)
|
||||
require.Len(t, result, 2)
|
||||
}
|
||||
|
||||
func TestBuildSoraContent_Image(t *testing.T) {
|
||||
content := buildSoraContent("image", []string{"https://a.com/1.png", "https://a.com/2.png"})
|
||||
require.Contains(t, content, "")
|
||||
require.Contains(t, content, "")
|
||||
}
|
||||
|
||||
func TestBuildSoraContent_Video(t *testing.T) {
|
||||
content := buildSoraContent("video", []string{"https://a.com/v.mp4"})
|
||||
require.Contains(t, content, "<video src='https://a.com/v.mp4'")
|
||||
}
|
||||
|
||||
func TestBuildSoraContent_VideoEmpty(t *testing.T) {
|
||||
content := buildSoraContent("video", nil)
|
||||
require.Empty(t, content)
|
||||
}
|
||||
|
||||
func TestBuildSoraContent_Prompt(t *testing.T) {
|
||||
content := buildSoraContent("prompt", nil)
|
||||
require.Empty(t, content)
|
||||
}
|
||||
|
||||
func TestSoraImageSizeFromModel(t *testing.T) {
|
||||
require.Equal(t, "360", soraImageSizeFromModel("gpt-image"))
|
||||
require.Equal(t, "540", soraImageSizeFromModel("gpt-image-landscape"))
|
||||
require.Equal(t, "540", soraImageSizeFromModel("gpt-image-portrait"))
|
||||
require.Equal(t, "540", soraImageSizeFromModel("something-landscape"))
|
||||
require.Equal(t, "360", soraImageSizeFromModel("unknown-model"))
|
||||
}
|
||||
|
||||
func TestFirstMediaURL(t *testing.T) {
|
||||
require.Equal(t, "", firstMediaURL(nil))
|
||||
require.Equal(t, "", firstMediaURL([]string{}))
|
||||
require.Equal(t, "a", firstMediaURL([]string{"a", "b"}))
|
||||
}
|
||||
|
||||
func TestSoraProErrorMessage(t *testing.T) {
|
||||
require.Contains(t, soraProErrorMessage("sora2pro-hd", ""), "Pro-HD")
|
||||
require.Contains(t, soraProErrorMessage("sora2pro", ""), "Pro")
|
||||
require.Empty(t, soraProErrorMessage("sora-basic", ""))
|
||||
}
|
||||
|
||||
func TestShouldFailoverUpstreamError(t *testing.T) {
|
||||
svc := NewSoraGatewayService(nil, nil, nil, &config.Config{})
|
||||
require.True(t, svc.shouldFailoverUpstreamError(401))
|
||||
require.True(t, svc.shouldFailoverUpstreamError(429))
|
||||
require.True(t, svc.shouldFailoverUpstreamError(500))
|
||||
require.True(t, svc.shouldFailoverUpstreamError(502))
|
||||
require.False(t, svc.shouldFailoverUpstreamError(200))
|
||||
require.False(t, svc.shouldFailoverUpstreamError(400))
|
||||
}
|
||||
|
||||
func TestWithSoraTimeout_NilService(t *testing.T) {
|
||||
var svc *SoraGatewayService
|
||||
ctx, cancel := svc.withSoraTimeout(context.Background(), false)
|
||||
require.NotNil(t, ctx)
|
||||
require.Nil(t, cancel)
|
||||
}
|
||||
|
||||
func TestWithSoraTimeout_ZeroTimeout(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
svc := NewSoraGatewayService(nil, nil, nil, cfg)
|
||||
ctx, cancel := svc.withSoraTimeout(context.Background(), false)
|
||||
require.NotNil(t, ctx)
|
||||
require.Nil(t, cancel)
|
||||
}
|
||||
|
||||
func TestWithSoraTimeout_PositiveTimeout(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Gateway: config.GatewayConfig{
|
||||
SoraRequestTimeoutSeconds: 30,
|
||||
},
|
||||
}
|
||||
svc := NewSoraGatewayService(nil, nil, nil, cfg)
|
||||
ctx, cancel := svc.withSoraTimeout(context.Background(), false)
|
||||
require.NotNil(t, ctx)
|
||||
require.NotNil(t, cancel)
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestPollInterval(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Client: config.SoraClientConfig{
|
||||
PollIntervalSeconds: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := NewSoraGatewayService(nil, nil, nil, cfg)
|
||||
require.Equal(t, 5*time.Second, svc.pollInterval())
|
||||
|
||||
// 默认值
|
||||
svc2 := NewSoraGatewayService(nil, nil, nil, &config.Config{})
|
||||
require.True(t, svc2.pollInterval() > 0)
|
||||
}
|
||||
|
||||
func TestPollMaxAttempts(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Client: config.SoraClientConfig{
|
||||
MaxPollAttempts: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := NewSoraGatewayService(nil, nil, nil, cfg)
|
||||
require.Equal(t, 100, svc.pollMaxAttempts())
|
||||
|
||||
// 默认值
|
||||
svc2 := NewSoraGatewayService(nil, nil, nil, &config.Config{})
|
||||
require.True(t, svc2.pollMaxAttempts() > 0)
|
||||
}
|
||||
|
||||
func TestDecodeSoraImageInput_BlockPrivateURL(t *testing.T) {
|
||||
_, _, err := decodeSoraImageInput(context.Background(), "http://127.0.0.1/internal.png")
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -12,6 +12,167 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSoraMediaCleanupService_RunCleanup_NilCfg(t *testing.T) {
|
||||
storage := &SoraMediaStorage{}
|
||||
svc := &SoraMediaCleanupService{storage: storage, cfg: nil}
|
||||
// 不应 panic
|
||||
svc.runCleanup()
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_RunCleanup_NilStorage(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
svc := &SoraMediaCleanupService{storage: nil, cfg: cfg}
|
||||
// 不应 panic
|
||||
svc.runCleanup()
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_RunCleanup_ZeroRetention(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
Type: "local",
|
||||
LocalPath: tmpDir,
|
||||
Cleanup: config.SoraStorageCleanupConfig{
|
||||
Enabled: true,
|
||||
RetentionDays: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
storage := NewSoraMediaStorage(cfg)
|
||||
svc := NewSoraMediaCleanupService(storage, cfg)
|
||||
// retention=0 应跳过清理
|
||||
svc.runCleanup()
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_NilCfg(t *testing.T) {
|
||||
svc := NewSoraMediaCleanupService(nil, nil)
|
||||
svc.Start() // cfg == nil 时应直接返回
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_StorageDisabled(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
Cleanup: config.SoraStorageCleanupConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := NewSoraMediaCleanupService(nil, cfg)
|
||||
svc.Start() // storage == nil 时应直接返回
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_WithTimezone(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
Timezone: "Asia/Shanghai",
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
Type: "local",
|
||||
LocalPath: tmpDir,
|
||||
Cleanup: config.SoraStorageCleanupConfig{
|
||||
Enabled: true,
|
||||
Schedule: "0 3 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
storage := NewSoraMediaStorage(cfg)
|
||||
svc := NewSoraMediaCleanupService(storage, cfg)
|
||||
svc.Start()
|
||||
t.Cleanup(svc.Stop)
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_Disabled(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
Cleanup: config.SoraStorageCleanupConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := NewSoraMediaCleanupService(nil, cfg)
|
||||
svc.Start() // 不应 panic,也不应启动 cron
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_NilSelf(t *testing.T) {
|
||||
var svc *SoraMediaCleanupService
|
||||
svc.Start() // 不应 panic
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_EmptySchedule(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
Type: "local",
|
||||
LocalPath: tmpDir,
|
||||
Cleanup: config.SoraStorageCleanupConfig{
|
||||
Enabled: true,
|
||||
Schedule: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
storage := NewSoraMediaStorage(cfg)
|
||||
svc := NewSoraMediaCleanupService(storage, cfg)
|
||||
svc.Start() // 空 schedule 不应启动
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_InvalidSchedule(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
Type: "local",
|
||||
LocalPath: tmpDir,
|
||||
Cleanup: config.SoraStorageCleanupConfig{
|
||||
Enabled: true,
|
||||
Schedule: "invalid-cron",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
storage := NewSoraMediaStorage(cfg)
|
||||
svc := NewSoraMediaCleanupService(storage, cfg)
|
||||
svc.Start() // 无效 schedule 不应 panic
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Start_ValidSchedule(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
Sora: config.SoraConfig{
|
||||
Storage: config.SoraStorageConfig{
|
||||
Type: "local",
|
||||
LocalPath: tmpDir,
|
||||
Cleanup: config.SoraStorageCleanupConfig{
|
||||
Enabled: true,
|
||||
Schedule: "0 3 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
storage := NewSoraMediaStorage(cfg)
|
||||
svc := NewSoraMediaCleanupService(storage, cfg)
|
||||
svc.Start()
|
||||
t.Cleanup(svc.Stop)
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Stop_NilSelf(t *testing.T) {
|
||||
var svc *SoraMediaCleanupService
|
||||
svc.Stop() // 不应 panic
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_Stop_WithoutStart(t *testing.T) {
|
||||
svc := NewSoraMediaCleanupService(nil, &config.Config{})
|
||||
svc.Stop() // cron 未启动时 Stop 不应 panic
|
||||
}
|
||||
|
||||
func TestSoraMediaCleanupService_RunCleanup(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -52,3 +53,81 @@ func TestSubscriptionMaintenanceQueue_TryEnqueue_PanicDoesNotKillWorker(t *testi
|
||||
t.Fatalf("worker did not continue after panic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionMaintenanceQueue_TryEnqueue_AfterStop(t *testing.T) {
|
||||
q := NewSubscriptionMaintenanceQueue(1, 8)
|
||||
q.Stop()
|
||||
|
||||
err := q.TryEnqueue(func() {})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "stopped")
|
||||
}
|
||||
|
||||
func TestSubscriptionMaintenanceQueue_TryEnqueue_NilReceiver(t *testing.T) {
|
||||
var q *SubscriptionMaintenanceQueue
|
||||
err := q.TryEnqueue(func() {})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "nil")
|
||||
}
|
||||
|
||||
func TestSubscriptionMaintenanceQueue_TryEnqueue_NilTask(t *testing.T) {
|
||||
q := NewSubscriptionMaintenanceQueue(1, 8)
|
||||
t.Cleanup(q.Stop)
|
||||
|
||||
err := q.TryEnqueue(nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "nil")
|
||||
}
|
||||
|
||||
func TestSubscriptionMaintenanceQueue_Stop_NilReceiver(t *testing.T) {
|
||||
var q *SubscriptionMaintenanceQueue
|
||||
// 不应 panic
|
||||
q.Stop()
|
||||
}
|
||||
|
||||
func TestSubscriptionMaintenanceQueue_Stop_Idempotent(t *testing.T) {
|
||||
q := NewSubscriptionMaintenanceQueue(1, 4)
|
||||
q.Stop()
|
||||
q.Stop() // 第二次调用不应 panic
|
||||
}
|
||||
|
||||
func TestNewSubscriptionMaintenanceQueue_ZeroParams(t *testing.T) {
|
||||
q := NewSubscriptionMaintenanceQueue(0, 0)
|
||||
t.Cleanup(q.Stop)
|
||||
|
||||
// workerCount/queueSize 应被修正为 1
|
||||
err := q.TryEnqueue(func() {})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNewSubscriptionMaintenanceQueue_NegativeParams(t *testing.T) {
|
||||
q := NewSubscriptionMaintenanceQueue(-1, -1)
|
||||
t.Cleanup(q.Stop)
|
||||
|
||||
err := q.TryEnqueue(func() {})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSubscriptionMaintenanceQueue_ConcurrentEnqueueAndStop(t *testing.T) {
|
||||
// 并发调用 TryEnqueue 和 Stop 不应 panic
|
||||
for i := 0; i < 100; i++ {
|
||||
q := NewSubscriptionMaintenanceQueue(2, 4)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 50; j++ {
|
||||
_ = q.TryEnqueue(func() {})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(time.Microsecond * time.Duration(i%10))
|
||||
q.Stop()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,41 @@ func TestTimingWheelService_Cancel_PreventsExecution(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimingWheelService_Schedule_AfterStop_LogsError(t *testing.T) {
|
||||
svc, err := NewTimingWheelService()
|
||||
if err != nil {
|
||||
t.Fatalf("期望 err 为 nil,但得到: %v", err)
|
||||
}
|
||||
svc.Stop()
|
||||
|
||||
// Stop 后调用 Schedule 应走 error 日志路径,不应 panic
|
||||
svc.Schedule("after-stop", 100*time.Millisecond, func() {
|
||||
t.Fatal("不应被执行")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTimingWheelService_ScheduleRecurring_AfterStop_LogsError(t *testing.T) {
|
||||
svc, err := NewTimingWheelService()
|
||||
if err != nil {
|
||||
t.Fatalf("期望 err 为 nil,但得到: %v", err)
|
||||
}
|
||||
svc.Stop()
|
||||
|
||||
// Stop 后调用 ScheduleRecurring 应走 error 日志路径,不应 panic
|
||||
svc.ScheduleRecurring("after-stop-rec", 100*time.Millisecond, func() {
|
||||
t.Fatal("不应被执行")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTimingWheelService_Stop_Idempotent(t *testing.T) {
|
||||
svc, err := NewTimingWheelService()
|
||||
if err != nil {
|
||||
t.Fatalf("期望 err 为 nil,但得到: %v", err)
|
||||
}
|
||||
svc.Stop()
|
||||
svc.Stop() // 第二次调用不应 panic
|
||||
}
|
||||
|
||||
func TestTimingWheelService_ScheduleRecurring_ExecutesMultipleTimes(t *testing.T) {
|
||||
original := newTimingWheel
|
||||
t.Cleanup(func() { newTimingWheel = original })
|
||||
|
||||
Reference in New Issue
Block a user