package voice import ( "bytes" "context" "errors" "os" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/transcode" ) // fakeTranscoder 是 Prepare 的能力门与转码调用的替身:它记录是否被调用、被问了哪条路, // 让「可播格式**绝不**碰转码」这类纪律成为可断言的事实(而不是靠读代码相信)。 type fakeTranscoder struct { // routes 是「源>目标」的可用集合;nil 表示按 DefaultCodecs 的口径只认 amr>wav。 routes map[string]bool result transcode.Result err error calls int lastReq transcode.Request } func routeKey(src, dst transcode.Format) string { return transcode.FormatName(src) + ">" + transcode.FormatName(dst) } func newFakeTranscoder() *fakeTranscoder { return &fakeTranscoder{routes: map[string]bool{routeKey(transcode.FormatAMR, transcode.FormatWAV): true}} } func (f *fakeTranscoder) Transcode(_ context.Context, req transcode.Request) (transcode.Result, error) { f.calls++ f.lastReq = req if f.err != nil { return transcode.Result{}, f.err } res := f.result res.Source = req.Source if res.Format == "" { res.Format = req.Target } return res, nil } func (f *fakeTranscoder) Supports(src, dst transcode.Format) bool { return f.routes[routeKey(src, dst)] } func (f *fakeTranscoder) Formats() []transcode.Format { return []transcode.Format{transcode.FormatAMR, transcode.FormatWAV} } // TestPrepareKeepsPlayableBytesAsIs 覆盖三态之①:源格式可播 ⇒ **原样落盘**,一个字节都 // 不改、也不碰转码(design/25 §7.1:不可播才转码;可播却转一圈等于白烧 CPU 还把音质 // 往下拽)。 func TestPrepareKeepsPlayableBytesAsIs(t *testing.T) { t.Parallel() tr := newFakeTranscoder() in := []byte("RIFF....WAVEfmt ") got, err := Prepare(context.Background(), tr, Prep{Data: in, Format: "WAV", DurationMs: 1500}) if err != nil { t.Fatalf("Prepare: %v", err) } if !bytes.Equal(got.Data, in) { t.Fatalf("字节被改动了:%q", got.Data) } if got.Format != "wav" || got.Transcoded { t.Fatalf("产物 = %+v, want 原样 wav / Transcoded=false", got) } if got.DurationMs != 1500 { t.Fatalf("时长 = %d, want 1500(渠道如实给出的值)", got.DurationMs) } if tr.calls != 0 { t.Fatalf("转码被调用 %d 次, want 0(可播格式绝不转码)", tr.calls) } } // TestPrepareTranscodesNonPlayableFormat 覆盖三态之②:不可播且转码可用 ⇒ 落**可播副本**, // 并且时长优先取转码产物**可确证**的值(渠道常给不出;转码按帧数算得出来)。 func TestPrepareTranscodesNonPlayableFormat(t *testing.T) { t.Parallel() tr := newFakeTranscoder() tr.result = transcode.Result{Data: []byte("RIFF-out"), Format: transcode.FormatWAV, DurationMs: 500} got, err := Prepare(context.Background(), tr, Prep{Data: []byte("#!AMR\n..."), Format: "amr"}) if err != nil { t.Fatalf("Prepare: %v", err) } if string(got.Data) != "RIFF-out" || got.Format != "wav" || !got.Transcoded { t.Fatalf("产物 = %+v, want 转码产物 wav/Transcoded=true", got) } if got.DurationMs != 500 { t.Fatalf("时长 = %d, want 500(转码产物可确证)", got.DurationMs) } if tr.calls != 1 { t.Fatalf("转码调用次数 = %d, want 1", tr.calls) } if tr.lastReq.Source != transcode.FormatAMR || tr.lastReq.Target != transcode.FormatWAV { t.Fatalf("请求 = %+v, want amr→wav", tr.lastReq) } } // TestPreparePrefersTranscodedDuration 覆盖时长来源的优先级:转码产物给不出时长时, // **沿用渠道如实给出的值**(绝不因为转码成功就把已知的时长抹成 0)。 func TestPreparePrefersTranscodedDuration(t *testing.T) { t.Parallel() tr := newFakeTranscoder() tr.result = transcode.Result{Data: []byte("RIFF-out"), Format: transcode.FormatWAV} got, err := Prepare(context.Background(), tr, Prep{Data: []byte("#!AMR\n"), Format: "amr", DurationMs: 3200}) if err != nil { t.Fatalf("Prepare: %v", err) } if got.DurationMs != 3200 { t.Fatalf("时长 = %d, want 3200(转码没给出 ⇒ 沿用渠道值)", got.DurationMs) } } // TestPrepareNegativeDurationIsTreatedAsUnknown 钉住「渠道给了个无意义的值」的处理: // 负数按**不知道**处理(0),既不采信也不编造 —— 落库前 Ref.Validate 会拒绝负数。 func TestPrepareNegativeDurationIsTreatedAsUnknown(t *testing.T) { t.Parallel() got, err := Prepare(context.Background(), nil, Prep{Data: []byte("RIFF"), Format: "wav", DurationMs: -5}) if err != nil { t.Fatalf("Prepare: %v", err) } if got.DurationMs != 0 { t.Fatalf("时长 = %d, want 0(负值 = 不知道)", got.DurationMs) } } // prepareFailureCases 是「不可播且转码不可用」的全部失败面(design/25 §2.4 / §5.4): // 每一条都要求「有错、可判类、无字节」。表放在用例之外,好让用例本体只表达纪律。 var prepareFailureCases = []struct { name string tr transcode.Transcoder in Prep wantErr error category string }{ { name: "没有字节", tr: newFakeTranscoder(), in: Prep{Data: nil, Format: "wav"}, wantErr: ErrNoBytes, category: "no_bytes", }, { name: "不可播且没有登记目标(silk)", tr: newFakeTranscoder(), in: Prep{Data: []byte("x"), Format: "silk"}, wantErr: ErrNotPlayable, category: "not_playable", }, { name: "不可播且格式不认识", tr: newFakeTranscoder(), in: Prep{Data: []byte("x"), Format: ""}, wantErr: ErrNotPlayable, category: "not_playable", }, { name: "转码能力未接线(amr 但组合根没注入)", tr: nil, in: Prep{Data: []byte("#!AMR\n"), Format: "amr"}, wantErr: ErrNoTranscoder, category: "transcode_not_wired", }, { name: "注册表里没有这条路(amr-wb 本期未实现)", tr: newFakeTranscoder(), in: Prep{Data: []byte("#!AMR-WB\n"), Format: "amr-wb"}, wantErr: transcode.ErrUnsupportedFormat, category: "unsupported_format", }, { name: "输入越限", tr: failingTranscoder(transcode.ErrInputTooLarge), in: Prep{Data: []byte("#!AMR\n"), Format: "amr"}, wantErr: transcode.ErrInputTooLarge, category: "input_too_large", }, { name: "产物越限", tr: failingTranscoder(transcode.ErrOutputTooLarge), in: Prep{Data: []byte("#!AMR\n"), Format: "amr"}, wantErr: transcode.ErrOutputTooLarge, category: "output_too_large", }, { name: "解码失败(字节损坏)", tr: failingTranscoder(transcode.ErrDecodeFailed), in: Prep{Data: []byte("#!AMR\n"), Format: "amr"}, wantErr: transcode.ErrDecodeFailed, category: "decode_failed", }, { name: "并发槽满(快速失败,不排队)", tr: failingTranscoder(transcode.ErrBusy), in: Prep{Data: []byte("#!AMR\n"), Format: "amr"}, wantErr: transcode.ErrBusy, category: "busy", }, { name: "超时", tr: failingTranscoder(transcode.ErrTimeout), in: Prep{Data: []byte("#!AMR\n"), Format: "amr"}, wantErr: transcode.ErrTimeout, category: "timeout", }, } // TestPrepareFailsHonestly 覆盖三态之③与全部失败分类(design/25 §2.4 / §5.4): // **不可播且转码不可用**时绝不返回一份播不了却当作可播的结果;错误必须可 errors.Is // 判定、可归类(Category 是「只进日志」的那份机器可读分类,design/24 §10.12-9)。 func TestPrepareFailsHonestly(t *testing.T) { t.Parallel() for _, tc := range prepareFailureCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() got, err := Prepare(context.Background(), tc.tr, tc.in) if err == nil { t.Fatalf("Prepare(%+v) = %+v, want 错误(绝不返回播不了却当作可播的结果)", tc.in, got) } if !errors.Is(err, tc.wantErr) { t.Fatalf("错误 = %v, want %v", err, tc.wantErr) } if cat := Category(err); cat != tc.category { t.Fatalf("Category(%v) = %q, want %q", err, cat, tc.category) } if len(got.Data) != 0 { t.Fatalf("失败时仍返回了字节:%q", got.Data) } }) } } // failingTranscoder 返回一个自称有 amr→wav 这条路、但一转就报指定哨兵的替身。 func failingTranscoder(err error) *fakeTranscoder { f := newFakeTranscoder() f.err = err return f } // TestPrepareRejectsEmptyTranscodeOutput 覆盖「转码器自称成功却没给字节」:那是目标侧 // 构造失败,绝不能当成一份空副本落盘(空文件播放不了,documents 侧也会按空文件拒掉)。 func TestPrepareRejectsEmptyTranscodeOutput(t *testing.T) { t.Parallel() tr := newFakeTranscoder() tr.result = transcode.Result{Format: transcode.FormatWAV} _, err := Prepare(context.Background(), tr, Prep{Data: []byte("#!AMR\n"), Format: "amr"}) if !errors.Is(err, transcode.ErrEncodeFailed) { t.Fatalf("错误 = %v, want transcode.ErrEncodeFailed", err) } if cat := Category(err); cat != "encode_failed" { t.Fatalf("Category = %q, want encode_failed", cat) } } // TestCategoryOfNilIsEmpty 钉住 Category(nil) 的空串:调用方按它决定「要不要记一条 // 失败日志」,因此成功必须是空串(否则每次成功转码都会刷一条 WARN)。 func TestCategoryOfNilIsEmpty(t *testing.T) { t.Parallel() if cat := Category(nil); cat != "" { t.Fatalf("Category(nil) = %q, want 空串", cat) } } // TestPrepareWithRealTranscodeEngine 是**真走**用例(design/25 §8-T 的「复用既有能力」 // 验收):用真实 `transcode.Engine` + `DefaultCodecs()` 把一份真 AMR-NB 夹具走通 // `amr → wav`,断言产物是 RIFF/WAVE 且时长**可确证**。 // // 夹具取自 transcode 包自己的 testdata(同一份,绝不再造一份):它是 opencore-amrnb // 编出的 0.5 秒合成信号,因此本用例不需要装 ffmpeg。若该目录被搬走,本用例会**红**而不是 // 静默跳过(「跳过」会让这条唯一的真走证据消失而没人发现)。 func TestPrepareWithRealTranscodeEngine(t *testing.T) { t.Parallel() const fixture = "../../capability/transcode/testdata/mode12200.amr" data, err := os.ReadFile(fixture) if err != nil { t.Fatalf("读夹具 %s: %v(真走用例依赖 transcode 包自己的 testdata)", fixture, err) } tr := transcode.NewEngine(transcode.DefaultLimits(), transcode.WithCodecs(transcode.DefaultCodecs()...)) got, err := Prepare(context.Background(), tr, Prep{Data: data, Format: "amr"}) if err != nil { t.Fatalf("Prepare(真 amr 夹具): %v", err) } if !got.Transcoded || got.Format != "wav" { t.Fatalf("产物 = %+v, want wav 转码产物", got) } if len(got.Data) < 44 || string(got.Data[:4]) != "RIFF" || string(got.Data[8:12]) != "WAVE" { t.Fatalf("产物不是 RIFF/WAVE:前 12 字节 = %q", head(got.Data, 12)) } if got.DurationMs <= 0 { t.Fatalf("时长 = %d, want >0(AMR 帧数可确证)", got.DurationMs) } } // head 返回 data 的前 n 字节(越界时返回全部),只用于失败信息。 func head(data []byte, n int) []byte { if len(data) < n { return data } return data[:n] }