复审 p2-2items(P2-1 跨进程去重按裁决只报不修):
(a) **关闭期 ctx 取消 ≠ 没起**:后端关闭时 ctx 被取消,探活返回
`context.Canceled`;原实现把它判成"没起" ⇒ **在关闭过程中拉起 TTS**。
现在按 `errors.Is(err, context.Canceled)` 判为跳过:不记 markAttempt(不消耗
冷却)、不拉、也不 WARN 成故障。探活**自身超时**(DeadlineExceeded)仍是"没起",
照常拉 —— 两者用 errors.Is 区分,不按文案。新增 OutcomeCanceled。
(b) **探活地址必须合法**:写成 `127.0.0.1:9880`(漏 scheme)会让 http.NewRequest
每次都失败 ⇒「没起」永远成立 ⇒ 每次启动都拉一次进程。新增
`ttslaunch.ValidateProbeURL`(url.Parse + scheme ∈ {http,https} + host 非空):
- `New` 里校验 ⇒ 不合法即 WARN + 不启用(兜底闸,覆盖任何调用方);
- 组合根 `validateResolvedProbeURL` 里同一把闸再校验一次,WARN 带
`probe_url_source` 指明该改哪个文件 —— **baseUrl 兜底那条路也被覆盖**。
顺带按复审记录项 1 修正 `Proxy: nil` 的注释:net/http 本来就不给回环 IP 走代理,
这行真正保护的是 probeUrl 被配成主机名的情况(原注释高估了它)。
用例(均变异验证打红):
- TestCheck_ParentCanceled_SkipsLaunch:取消 ⇒ OutcomeCanceled + 0 次拉起 + 不进冷却
(变异:跳过改回"没起" ⇒ Outcome=launched、marker 落地)
- TestCheck_ProbeOwnTimeout_StillLaunches:探活自身超时仍照常拉
- TestNew_InvalidProbeURL_Disables:7 种非法形状 ⇒ 不接单、0 次拉起、有 WARN
- TestStartVoiceTTSLaunch_{ShutdownCancelDoesNotLaunch,InvalidProbeURLStaysOff,
InvalidBaseURLFallbackStaysOff}:组合根三条,均用真 .bat marker 观察"有没有拉起"
130 lines
4.7 KiB
Go
130 lines
4.7 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|