package transcode import ( "bytes" "context" "errors" "fmt" "log/slog" "strings" "sync/atomic" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // stubCodec 是 engine 测试用的编解码器桩:它不碰真实音频,只记录调用次数并按 // 需要阻塞 / 报错,因此可以精确检验「闸在解码之前」这类顺序性断言。 type stubCodec struct { from, to Format calls atomic.Int32 // entered 在 Transcode 进入时被关闭(并发用例用来确认槽已被占)。 entered chan struct{} // block 非 nil 时,Transcode 会等到它被关闭或 ctx 结束。 block chan struct{} // waitCtx 为 true 时,Transcode 会一直等到 ctx 结束(超时用例)。 waitCtx bool err error result Result } func newStubCodec() *stubCodec { return &stubCodec{from: FormatAMR, to: FormatWAV} } func (c *stubCodec) From() Format { return c.from } func (c *stubCodec) To() Format { return c.to } func (c *stubCodec) Transcode(ctx context.Context, in []byte, _ Limits) (Result, error) { c.calls.Add(1) if c.entered != nil { select { case c.entered <- struct{}{}: default: } } if c.block != nil { select { case <-c.block: case <-ctx.Done(): return Result{}, ctx.Err() } } if c.waitCtx { <-ctx.Done() return Result{}, ctx.Err() } if c.err != nil { return Result{}, c.err } res := c.result if res.Data == nil { res.Data = in } if res.Source == "" { res.Source = c.from } if res.Format == "" { res.Format = c.to } return res, nil } // amrStubBytes 是一段「魔数与目标路都对得上」的最小输入。 func amrStubBytes(payload ...byte) []byte { return append([]byte(amrNBMagic), payload...) } // TestEngineEmptyRegistry 是 design/25 §8-T1 的空注册表行为:Supports 恒 false、 // Transcode 恒 ErrUnsupportedFormat(且 Formats 为空)。 func TestEngineEmptyRegistry(t *testing.T) { eng := NewEngine(DefaultLimits()) if eng.Supports(FormatAMR, FormatWAV) { t.Fatalf("empty registry reports support for amr -> wav") } if got := eng.Formats(); len(got) != 0 { t.Fatalf("empty registry formats = %v, want none", got) } _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }) if !errors.Is(err, ErrUnsupportedFormat) { t.Fatalf("err = %v, want ErrUnsupportedFormat", err) } } // TestEngineInputTooLargeDecodesNothing 断言输入字节闸在**编解码器之前**:桩一次都 // 没被调用(design/25 §8-T1「越限 ⇒ ErrInputTooLarge 且一行都没解码」)。 func TestEngineInputTooLargeDecodesNothing(t *testing.T) { stub := newStubCodec() limits := DefaultLimits() limits.MaxInputBytes = len(amrNBMagic) eng := NewEngine(limits, WithCodecs(stub)) data := amrStubBytes(0x3c, 0x00) _, err := eng.Transcode(context.Background(), Request{Data: data, Source: FormatAMR, Target: FormatWAV}) if !errors.Is(err, ErrInputTooLarge) { t.Fatalf("err = %v, want ErrInputTooLarge", err) } if n := stub.calls.Load(); n != 0 { t.Fatalf("codec was invoked %d times before the input-size gate", n) } } // TestEngineBusyFailsFast 断言并发槽满时**快速失败、不入队**(design/25 §5.2), // 且槽随后会被归还。 func TestEngineBusyFailsFast(t *testing.T) { stub := newStubCodec() stub.block = make(chan struct{}) stub.entered = make(chan struct{}, 1) limits := DefaultLimits() limits.Concurrency = 1 eng := NewEngine(limits, WithCodecs(stub)) done := make(chan error, 1) go func() { _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }) done <- err }() <-stub.entered start := time.Now() _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }) if !errors.Is(err, ErrBusy) { t.Fatalf("err = %v, want ErrBusy", err) } if elapsed := time.Since(start); elapsed > busyFailBudget { t.Fatalf("busy rejection took %s (should fail fast, not queue)", elapsed) } close(stub.block) if err := <-done; err != nil { t.Fatalf("first call: %v", err) } if _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }); err != nil { t.Fatalf("slot not released after the first call: %v", err) } } // busyFailBudget 是「快速失败」的时间预算:ErrBusy 必须来自非阻塞的 select, // 而不是等出来的。取值远大于调度抖动(同一台机器上实测 <1ms)。 const busyFailBudget = 500 * time.Millisecond // TestEngineTimeout 断言单次 ctx 超时是硬闸:超时即 ErrTimeout,且调用方拿得到 // (不无界等待、不泄漏 goroutine)。 func TestEngineTimeout(t *testing.T) { stub := newStubCodec() stub.waitCtx = true limits := DefaultLimits() limits.Timeout = 20 * time.Millisecond eng := NewEngine(limits, WithCodecs(stub)) start := time.Now() _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }) if !errors.Is(err, ErrTimeout) { t.Fatalf("err = %v, want ErrTimeout", err) } if elapsed := time.Since(start); elapsed > time.Second { t.Fatalf("timeout took %s, want ~%s", elapsed, limits.Timeout) } } // TestEngineRejectsBadRequests 覆盖「进入编解码器之前」的几种诚实拒绝。 func TestEngineRejectsBadRequests(t *testing.T) { eng := NewEngine(DefaultLimits(), WithCodecs(newStubCodec())) cases := []struct { name string req Request }{ {"empty target", Request{Data: amrStubBytes(0x3c), Source: FormatAMR}}, {"source not sniffable", Request{Data: []byte("not audio at all"), Target: FormatWAV}}, {"no route for sniffed source", Request{Data: []byte("RIFF\x00\x00\x00\x00WAVE"), Target: FormatWAV}}, {"unknown source name", Request{Data: amrStubBytes(0x3c), Source: Format("mp3"), Target: FormatWAV}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := eng.Transcode(context.Background(), tc.req) if !errors.Is(err, ErrUnsupportedFormat) { t.Fatalf("err = %v, want ErrUnsupportedFormat", err) } }) } } // TestEngineSniffsSourceWhenOmitted 断言 Source 留空时按魔数嗅探并回填到 Result。 func TestEngineSniffsSourceWhenOmitted(t *testing.T) { eng := NewEngine(DefaultLimits(), WithCodecs(newStubCodec())) res, err := eng.Transcode(context.Background(), Request{Data: amrStubBytes(0x3c), Target: FormatWAV}) if err != nil { t.Fatalf("transcode: %v", err) } if res.Source != FormatAMR { t.Fatalf("sniffed source = %q, want amr", res.Source) } } // TestEngineCodecContractViolations 断言「不该发生的」被当成失败:空产物 ⇒ // ErrEncodeFailed;非哨兵错误 ⇒ 原样上抛(但记 ERROR 级日志,绝不静默)。 func TestEngineCodecContractViolations(t *testing.T) { t.Run("empty output", func(t *testing.T) { stub := newStubCodec() stub.result = Result{Data: []byte{}} stub.err = nil eng := NewEngine(DefaultLimits(), WithCodecs(stub)) _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }) if !errors.Is(err, ErrEncodeFailed) { t.Fatalf("err = %v, want ErrEncodeFailed", err) } }) t.Run("non-sentinel error is not swallowed", func(t *testing.T) { var buf bytes.Buffer stub := newStubCodec() stub.err = errBoom eng := NewEngine(DefaultLimits(), WithCodecs(stub), WithLogger(testLogger(&buf))) _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }) if !errors.Is(err, errBoom) { t.Fatalf("err = %v, want the codec error to pass through", err) } assertLogContains(t, &buf, "level=ERROR", "transcode failed", "source=capability/transcode") }) } // errBoom 是「编解码器违约」用的非哨兵错误。 var errBoom = errors.New("boom") // testLogger 造一个按组合根口径接线的日志器(source=capability/transcode)。 func testLogger(buf *bytes.Buffer) *slog.Logger { return logger.WithSource(slog.New(slog.NewTextHandler(buf, nil)), logSource) } // logSource 是本能力的日志来源(design/25 §5.3;组合根必须照此接线)。 const logSource = "capability/transcode" // assertLogContains 断言日志里出现了给定的每一段子串。 func assertLogContains(t *testing.T, buf *bytes.Buffer, wants ...string) { t.Helper() got := buf.String() for _, want := range wants { if !strings.Contains(got, want) { t.Fatalf("log missing %q; got:\n%s", want, got) } } } // TestEngineLogsEveryOutcome 断言 design/25 §5.3 要求的字段与来源都在: // 成功记 Info(含 in/out 字节数、帧数、时长、耗时),失败记 Warn。 // 同时断言「只记长度、不记字节内容」。 func TestEngineLogsEveryOutcome(t *testing.T) { t.Run("success", func(t *testing.T) { var buf bytes.Buffer stub := newStubCodec() eng := NewEngine(DefaultLimits(), WithCodecs(stub), WithLogger(testLogger(&buf))) if _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }); err != nil { t.Fatalf("transcode: %v", err) } assertLogContains(t, &buf, "level=INFO", "transcode ok", "source=capability/transcode", "source_format=amr", "target_format=wav", "in_bytes=7", "out_bytes=7", "frames=0") }) t.Run("unsupported is a warning", func(t *testing.T) { var buf bytes.Buffer eng := NewEngine(DefaultLimits(), WithLogger(testLogger(&buf))) if _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }); !errors.Is(err, ErrUnsupportedFormat) { t.Fatalf("err = %v", err) } assertLogContains(t, &buf, "level=WARN", "transcode failed", "source=capability/transcode") }) } // TestEngineRegistrationErrorsAreVisible 断言注册失败不阻塞构造、但**可见** // (绝不静默):nil 注册项、重复注册都会进 RegistrationError,且好的一路仍可用。 func TestEngineRegistrationErrorsAreVisible(t *testing.T) { cases := []struct { name string codecs []Codec wantErr error }{ {"nil codec", []Codec{nil, newStubCodec()}, ErrInvalidCodec}, {"duplicate codec", []Codec{newStubCodec(), newStubCodec()}, ErrDuplicate}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer eng := NewEngine(DefaultLimits(), WithCodecs(tc.codecs...), WithLogger(testLogger(&buf))) err := eng.RegistrationError() if !errors.Is(err, tc.wantErr) { t.Fatalf("RegistrationError = %v, want %v", err, tc.wantErr) } if !eng.Supports(FormatAMR, FormatWAV) { t.Fatalf("good codec was not registered") } assertLogContains(t, &buf, "level=ERROR", "transcode codec registration skipped") }) } } // TestEngineDefaultLimits 断言零值 Limits 被补成 design/25 §2.5 的具名默认, // 且显式给出的值不被覆盖。 func TestEngineDefaultLimits(t *testing.T) { got := NewEngine(Limits{}).Limits() want := DefaultLimits() if got != want { t.Fatalf("zero limits = %+v, want defaults %+v", got, want) } custom := Limits{MaxInputBytes: 1, MaxOutputBytes: 2, MaxDuration: 3, Concurrency: 4, Timeout: 5} if got := NewEngine(custom).Limits(); got != custom { t.Fatalf("explicit limits = %+v, want untouched %+v", got, custom) } } // TestEngineNilContextIsSafe 断言 ctx 为 nil 不会 panic(按 Background 处理)。 func TestEngineNilContextIsSafe(t *testing.T) { eng := NewEngine(DefaultLimits(), WithCodecs(newStubCodec())) if _, err := eng.Transcode(nil, Request{ //nolint:staticcheck // 故意传 nil ctx Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }); err != nil { t.Fatalf("nil ctx: %v", err) } } // TestEngineErrorsAreWrapped 断言对外错误都能被 errors.Is 命中哨兵,且带上下文 // (不是裸 err):文案形状是 `transcode: <源> -> <目标>: <原因>`。 func TestEngineErrorsAreWrapped(t *testing.T) { stub := newStubCodec() stub.err = fmt.Errorf("stub: %w", ErrDecodeFailed) eng := NewEngine(DefaultLimits(), WithCodecs(stub)) _, err := eng.Transcode(context.Background(), Request{ Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, }) if !errors.Is(err, ErrDecodeFailed) { t.Fatalf("err = %v, want ErrDecodeFailed", err) } if !strings.Contains(err.Error(), "stub") { t.Fatalf("wrapped error lost its cause: %v", err) } const wantPrefix = "transcode: amr -> wav: " if !strings.HasPrefix(err.Error(), wantPrefix) { t.Fatalf("err = %q, want the %q prefix(带路由上下文)", err.Error(), wantPrefix) } }