package ttslaunch // 本文件锁定复审 P2-2 的两条收口(都是"改坏就会静默拉进程"的那种): // // 1. **调用方 ctx 取消 ≠ 没起**:后端关闭时探活被打断,必须**跳过**(不拉、不记 // 冷却、不当故障),否则会在关闭过程中拉起 TTS; // 2. **探活地址必须合法**:漏 scheme / 拼错主机会让"没起"永远成立 ⇒ 每次启动都 // 拉一次进程。非法地址 ⇒ WARN + 不启用,一次都不拉。 import ( "context" "fmt" "strings" "sync/atomic" "testing" "time" ) // TestCheck_ParentCanceled_SkipsLaunch 钉住复审 P2-2a: // 探活挂在黑洞地址时取消父 ctx ⇒ 探活返回 context.Canceled ⇒ **跳过**。 // // 变异点:把"取消"改回判"没起" ⇒ spawned 变 1、Outcome 变 launched/failed,本用例红。 func TestCheck_ParentCanceled_SkipsLaunch(t *testing.T) { var spawned int32 l := New(Config{Enabled: true, ProbeTimeout: time.Second}, WithProbe(func(ctx context.Context) error { <-ctx.Done() return fmt.Errorf("ttslaunch: probe: %w", ctx.Err()) }), WithSpawn(countingSpawn(&spawned))) ctx, cancel := context.WithCancel(context.Background()) done := make(chan Outcome, 1) go func() { done <- l.Check(ctx) }() time.Sleep(50 * time.Millisecond) cancel() select { case got := <-done: if got != OutcomeCanceled { t.Fatalf("Outcome = %v, want canceled(关闭期取消不该判成没起)", got) } case <-time.After(2 * time.Second): t.Fatal("取消后 Check 没返回") } if n := atomic.LoadInt32(&spawned); n != 0 { t.Fatalf("关闭期取消仍拉起了 %d 次(want 0)", n) } // 取消不是一次"尝试":不该消耗冷却,否则关闭期恰好撞上冷却会让重启后第一次 // 探活被跳过。 if l.cooling() { t.Fatal("调用方取消被记成了一次拉起尝试(进了冷却)") } } // TestCheck_ProbeOwnTimeout_StillLaunches 钉住与上一条的分界线:**探活自身超时** // (context.DeadlineExceeded)仍然是"没起",要照常拉。 // // 变异点:把"任何 ctx 错误都跳过"写进去 ⇒ 本用例红(变成 canceled、spawned=0)。 func TestCheck_ProbeOwnTimeout_StillLaunches(t *testing.T) { var spawned int32 l := New(Config{Enabled: true}, WithProbe(func(context.Context) error { return fmt.Errorf("ttslaunch: probe: %w", context.DeadlineExceeded) }), WithSpawn(countingSpawn(&spawned))) if got := l.Check(context.Background()); got != OutcomeLaunched { t.Fatalf("Outcome = %v, want launched(探活自身超时 = 没起)", got) } if n := atomic.LoadInt32(&spawned); n != 1 { t.Fatalf("拉起 %d 次, want 1", n) } } // TestNew_InvalidProbeURL_Disables 钉住复审 P2-2b:非法探活地址 ⇒ WARN + **不启用**, // 一次探活都不发、一次都不拉。 // // 变异点:去掉 New 里的 ValidateProbeURL ⇒ 这些地址下 Trigger 会接单(最终每轮 // 都去拉进程),本用例红。 func TestNew_InvalidProbeURL_Disables(t *testing.T) { cases := []string{ "127.0.0.1:9880", // 漏 scheme(url.Parse 把 127.0.0.1 当 scheme) "localhost:9880", // 同上 "//127.0.0.1:9880", // 协议相对,没有 scheme "http://", // 有 scheme 没主机 "ftp://127.0.0.1:9880", // scheme 不对 "http://tts local:9880", // 空格 "http://[::1", // 未闭合方括号 } for _, raw := range cases { t.Run(raw, func(t *testing.T) { var spawned int32 log, buf := captureLogger() l := New(Config{Enabled: true, ProbeURL: raw}, WithLogger(log), WithProbe(func(context.Context) error { t.Error("非法地址不该再发探活") return nil }), WithSpawn(countingSpawn(&spawned))) if l.Trigger(context.Background()) { t.Fatal("非法 probeUrl 仍接单了(会每次启动都拉一次进程)") } if got := l.Check(context.Background()); got != OutcomeDisabled { t.Fatalf("Check = %v, want disabled", got) } l.Wait() if n := atomic.LoadInt32(&spawned); n != 0 { t.Fatalf("非法地址却拉起了 %d 次", n) } if out := buf.String(); !strings.Contains(out, "invalid probe url") { t.Fatalf("非法地址没记 WARN: %s", out) } }) } } // TestNew_ValidProbeURL_StaysEnabled 是对照组:合法地址(含主机名与 https)必须 // 照常启用 —— 免得把校验写成"一律不启用"。 func TestNew_ValidProbeURL_StaysEnabled(t *testing.T) { for _, raw := range []string{"http://127.0.0.1:9880", "https://tts.local:9880/health"} { l := New(Config{Enabled: true, ProbeURL: raw}, WithProbe(func(context.Context) error { return nil })) if got := l.Check(context.Background()); got != OutcomeAlreadyRunning { t.Fatalf("%s: Outcome = %v, want already_running", raw, got) } } }