package main
|
|
import (
|
"strings"
|
"sync/atomic"
|
"testing"
|
"time"
|
|
"voicesnap/internal/engine"
|
)
|
|
func TestLiveCaptionDisplayTextFitsOverlayBuffer(t *testing.T) {
|
input := strings.Repeat("这是一句实时字幕", 12)
|
got := liveCaptionDisplayText(input)
|
if got == "" {
|
t.Fatal("caption should not be empty")
|
}
|
if len([]byte(got)) > liveCaptionMaxBytes {
|
t.Fatalf("caption bytes = %d, want <= %d", len([]byte(got)), liveCaptionMaxBytes)
|
}
|
if !strings.HasPrefix(got, "...") {
|
t.Fatalf("caption = %q, want leading truncation marker", got)
|
}
|
}
|
|
func TestReleaseTailCaptureDelayUsesEngineCapability(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(tailCaptureTestEngine{delay: 300 * time.Millisecond})
|
|
if got := a.releaseTailCaptureDelay(); got != 300*time.Millisecond {
|
t.Fatalf("releaseTailCaptureDelay() = %v, want 300ms", got)
|
}
|
}
|
|
func TestReleaseTailCaptureDelayCanBeDisabledByEngineCapability(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(tailCaptureTestEngine{delay: 0})
|
|
if got := a.releaseTailCaptureDelay(); got != 0 {
|
t.Fatalf("releaseTailCaptureDelay() = %v, want 0", got)
|
}
|
}
|
|
func TestReleaseTailCaptureDelayDefaultsToNoProtectionWindow(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(plainTestEngine{})
|
|
if got := a.releaseTailCaptureDelay(); got != 0 {
|
t.Fatalf("releaseTailCaptureDelay() = %v, want 0", got)
|
}
|
}
|
|
func TestReleaseTailCaptureDelayDisabledWithoutEngine(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(nil)
|
|
if got := a.releaseTailCaptureDelay(); got != 0 {
|
t.Fatalf("releaseTailCaptureDelay() = %v, want 0 without engine", got)
|
}
|
}
|
|
func TestSlowRecognitionReason(t *testing.T) {
|
tests := []struct {
|
name string
|
recognizeMS int64
|
totalMS int64
|
want string
|
}{
|
{name: "fast", recognizeMS: 2000, totalMS: 2500, want: ""},
|
{name: "recognize", recognizeMS: 2001, totalMS: 2400, want: "recognize"},
|
{name: "total", recognizeMS: 1000, totalMS: 2501, want: "total"},
|
{name: "both", recognizeMS: 2001, totalMS: 2501, want: "recognize,total"},
|
}
|
|
for _, tt := range tests {
|
t.Run(tt.name, func(t *testing.T) {
|
if got := slowRecognitionReason(tt.recognizeMS, tt.totalMS); got != tt.want {
|
t.Fatalf("slowRecognitionReason(%d, %d) = %q, want %q", tt.recognizeMS, tt.totalMS, got, tt.want)
|
}
|
})
|
}
|
}
|
|
func TestHoldPreCaptureUsesEngineCapability(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(holdPreCaptureTestEngine{enabled: true})
|
|
if !a.holdPreCaptureEnabled.Load() {
|
t.Fatal("hold pre-capture should be enabled by engine capability")
|
}
|
}
|
|
func TestHoldPreCaptureCanBeDisabledByEngineCapability(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(holdPreCaptureTestEngine{enabled: false})
|
|
if a.holdPreCaptureEnabled.Load() {
|
t.Fatal("hold pre-capture should respect engine capability false")
|
}
|
}
|
|
func TestHoldPreCaptureDefaultsToTrueForReadyEngine(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(plainTestEngine{})
|
|
if !a.holdPreCaptureEnabled.Load() {
|
t.Fatal("hold pre-capture should default to true for ready engines")
|
}
|
}
|
|
func TestHoldPreCaptureDisabledWithoutEngine(t *testing.T) {
|
a := &App{}
|
a.replaceEngine(nil)
|
|
if a.holdPreCaptureEnabled.Load() {
|
t.Fatal("hold pre-capture should be disabled without an engine")
|
}
|
}
|
|
func TestHasEngineReflectsCurrentEngine(t *testing.T) {
|
a := &App{}
|
if a.hasEngine() {
|
t.Fatal("new app should not report an engine")
|
}
|
|
a.replaceEngine(plainTestEngine{})
|
if !a.hasEngine() {
|
t.Fatal("app should report an engine after replaceEngine")
|
}
|
|
a.replaceEngine(nil)
|
if a.hasEngine() {
|
t.Fatal("app should not report an engine after clearing")
|
}
|
}
|
|
func TestInitEngineSerializesAndDiscardsStaleEngine(t *testing.T) {
|
first := &closeTrackingTestEngine{name: "first"}
|
second := &closeTrackingTestEngine{name: "second"}
|
firstStarted := make(chan struct{})
|
allowFirst := make(chan struct{})
|
calls := atomic.Int32{}
|
|
a := &App{
|
engineFactory: func(initID uint64) (engine.Engine, engineMetadata, bool, error) {
|
call := calls.Add(1)
|
switch call {
|
case 1:
|
close(firstStarted)
|
<-allowFirst
|
return first, engineMetadata{InitID: initID, ResolvedModelID: "first", HardwareInfo: "first"}, true, nil
|
case 2:
|
return second, engineMetadata{InitID: initID, ResolvedModelID: "second", HardwareInfo: "second"}, true, nil
|
default:
|
t.Fatalf("unexpected engine factory call %d", call)
|
return nil, engineMetadata{}, false, nil
|
}
|
},
|
}
|
|
doneFirst := make(chan struct{})
|
go func() {
|
a.initEngine()
|
close(doneFirst)
|
}()
|
|
select {
|
case <-firstStarted:
|
case <-time.After(time.Second):
|
t.Fatal("first engine init did not start")
|
}
|
|
doneSecond := make(chan struct{})
|
go func() {
|
a.initEngine()
|
close(doneSecond)
|
}()
|
|
waitForCondition(t, time.Second, func() bool {
|
return a.engineInitSeq.Load() == 2
|
})
|
close(allowFirst)
|
|
select {
|
case <-doneFirst:
|
case <-time.After(time.Second):
|
t.Fatal("first engine init did not finish")
|
}
|
select {
|
case <-doneSecond:
|
case <-time.After(time.Second):
|
t.Fatal("second engine init did not finish")
|
}
|
|
a.engineMu.Lock()
|
gotEngine := a.eng
|
gotMeta := a.engineMeta
|
a.engineMu.Unlock()
|
|
if gotEngine != second {
|
t.Fatalf("final engine = %#v, want second", gotEngine)
|
}
|
if !first.closed.Load() {
|
t.Fatal("stale first engine should be closed")
|
}
|
if second.closed.Load() {
|
t.Fatal("current second engine should remain open")
|
}
|
if gotMeta.ResolvedModelID != "second" || gotMeta.InitID != 2 {
|
t.Fatalf("engine metadata = %+v, want second init metadata", gotMeta)
|
}
|
}
|
|
func waitForCondition(t *testing.T, timeout time.Duration, fn func() bool) {
|
t.Helper()
|
deadline := time.Now().Add(timeout)
|
for time.Now().Before(deadline) {
|
if fn() {
|
return
|
}
|
time.Sleep(10 * time.Millisecond)
|
}
|
t.Fatal("condition was not met before timeout")
|
}
|
|
func TestShouldStartHoldPreCaptureLocked(t *testing.T) {
|
a := &App{}
|
a.holdPreCaptureEnabled.Store(true)
|
|
if !a.shouldStartHoldPreCaptureLocked() {
|
t.Fatal("expected hold pre-capture to start when enabled and idle")
|
}
|
|
a.isRecording = true
|
if a.shouldStartHoldPreCaptureLocked() {
|
t.Fatal("should not pre-capture while already recording")
|
}
|
a.isRecording = false
|
|
a.isStoppingRecording = true
|
if a.shouldStartHoldPreCaptureLocked() {
|
t.Fatal("should not pre-capture while stopping")
|
}
|
a.isStoppingRecording = false
|
|
a.isFreetalking = true
|
if a.shouldStartHoldPreCaptureLocked() {
|
t.Fatal("should not pre-capture while tap recording is active")
|
}
|
a.isFreetalking = false
|
|
a.lastStopTime = time.Now()
|
if a.shouldStartHoldPreCaptureLocked() {
|
t.Fatal("should not pre-capture during post-stop debounce")
|
}
|
}
|
|
func TestStartRecordingIgnoredWhileStoppingRecording(t *testing.T) {
|
a := &App{isStoppingRecording: true}
|
|
a.startRecordingLocked()
|
|
if a.isRecording {
|
t.Fatal("startRecordingLocked should not start while stop tail capture is pending")
|
}
|
}
|
|
func TestStartTapRecordingIgnoredWhileStoppingRecording(t *testing.T) {
|
a := &App{isStoppingRecording: true}
|
|
a.startTapRecordingLocked()
|
|
if a.isRecording || a.isFreetalking {
|
t.Fatal("startTapRecordingLocked should not start while stop tail capture is pending")
|
}
|
}
|
|
type plainTestEngine struct{}
|
|
func (plainTestEngine) Recognize([]float32) (string, error) { return "", nil }
|
func (plainTestEngine) HardwareInfo() string { return "test" }
|
func (plainTestEngine) Close() {}
|
|
type closeTrackingTestEngine struct {
|
plainTestEngine
|
name string
|
closed atomic.Bool
|
}
|
|
func (e *closeTrackingTestEngine) HardwareInfo() string { return e.name }
|
func (e *closeTrackingTestEngine) Close() { e.closed.Store(true) }
|
|
type tailCaptureTestEngine struct {
|
plainTestEngine
|
delay time.Duration
|
}
|
|
func (e tailCaptureTestEngine) ReleaseTailCaptureDelay() time.Duration {
|
return e.delay
|
}
|
|
type holdPreCaptureTestEngine struct {
|
plainTestEngine
|
enabled bool
|
}
|
|
func (e holdPreCaptureTestEngine) HoldPreCaptureEnabled() bool {
|
return e.enabled
|
}
|