feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 13:02:28 +08:00
1 parent 89715d1813
commit 4a2d276881
100 files changed
+11319

No files matched your search

@@ -0,0 +1,360 @@
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)
}
}