| | |
| | | package main |
| | | |
| | | import ( |
| | | "context" |
| | | "errors" |
| | | "strings" |
| | | "sync/atomic" |
| | | "testing" |
| | | "time" |
| | | |
| | | "voicesnap/internal/config" |
| | | "voicesnap/internal/engine" |
| | | "voicesnap/internal/history" |
| | | "voicesnap/internal/overlay" |
| | | "voicesnap/internal/textoutput" |
| | | "voicesnap/internal/userdict" |
| | | ) |
| | | |
| | | func TestLiveCaptionDisplayTextFitsOverlayBuffer(t *testing.T) { |
| | |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureAutoConfirmsWithoutAdditionalKeyDown(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | a.hk.(*fakeHotkeyListener).down.Store(true) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.hotkeyActive |
| | | }) |
| | | |
| | | if got := a.recorder.(*fakeAppRecorder).starts.Load(); got != 1 { |
| | | t.Fatalf("recorder starts = %d, want 1", got) |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusRecording) { |
| | | t.Fatalf("indicator status = %q, want %q", got, overlay.StatusRecording) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureTimerConfirmsWhileRecorderStartBlocked(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | a.hk.(*fakeHotkeyListener).down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | blockStart := make(chan struct{}) |
| | | recorder.startBlock = blockStart |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return recorder.starts.Load() == 1 |
| | | }) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.hotkeyActive |
| | | }) |
| | | |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusRecording) { |
| | | t.Fatalf("indicator status = %q, want %q while recorder start is blocked", got, overlay.StatusRecording) |
| | | } |
| | | close(blockStart) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isHoldRecorderStarting |
| | | }) |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleaseAfterConfirmRunsRecognitionWhileRecorderStartBlocked(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | blockStart := make(chan struct{}) |
| | | recorder.startBlock = blockStart |
| | | recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} |
| | | recorder.hasVoice.Store(true) |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | paster := &fakeAppPaster{} |
| | | a.outputRouter = textoutput.NewRouter(paster) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | |
| | | listener.down.Store(false) |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return paster.pasted.Load() == "PrivateVoice AX POC" |
| | | }) |
| | | |
| | | if got := recorder.stopAndGetSamples.Load(); got != 1 { |
| | | t.Fatalf("StopAndGetSamples while recorder start is blocked = %d, want 1", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 1 { |
| | | t.Fatalf("recognizes = %d, want 1", got) |
| | | } |
| | | a.mu.Lock() |
| | | if a.isHoldStopPending { |
| | | t.Fatal("normal release while recorder is starting should not mark cancel-style pending stop") |
| | | } |
| | | a.mu.Unlock() |
| | | |
| | | close(blockStart) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | if got := recorder.stops.Load(); got != 0 { |
| | | t.Fatalf("app-level fake recorder stops = %d, want 0; native cleanup is covered by audio recorder tests", got) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleaseAfterConfirmRunsRecognitionBeforeRecorderStartEntered(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | preStartBlock := make(chan struct{}) |
| | | recorder.preStartBlock = preStartBlock |
| | | recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} |
| | | recorder.hasVoice.Store(true) |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | paster := &fakeAppPaster{} |
| | | a.outputRouter = textoutput.NewRouter(paster) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | if got := recorder.starts.Load(); got != 0 { |
| | | t.Fatalf("recorder Start entered before release = %d, want 0", got) |
| | | } |
| | | |
| | | listener.down.Store(false) |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return paster.pasted.Load() == "PrivateVoice AX POC" |
| | | }) |
| | | if got := recorder.stopAndGetSamples.Load(); got != 1 { |
| | | t.Fatalf("StopAndGetSamples before recorder Start entered = %d, want 1", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 1 { |
| | | t.Fatalf("recognizes = %d, want 1", got) |
| | | } |
| | | |
| | | close(preStartBlock) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | if got := recorder.stops.Load(); got != 0 { |
| | | t.Fatalf("app-level fake recorder stops = %d, want 0 for normal release pipeline", got) |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusDone) { |
| | | t.Fatalf("indicator status = %q, want %q", got, overlay.StatusDone) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleaseWaitsForStartupSamplesBeforeRecognition(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | blockStart := make(chan struct{}) |
| | | readSamplesReady := make(chan struct{}) |
| | | recorder.startBlock = blockStart |
| | | recorder.readSamplesBlock = readSamplesReady |
| | | recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} |
| | | recorder.hasVoice.Store(true) |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | paster := &fakeAppPaster{} |
| | | a.outputRouter = textoutput.NewRouter(paster) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | |
| | | listener.down.Store(false) |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | time.Sleep(holdStartupSamplePollInterval * 2) |
| | | if got := recorder.stopAndGetSamples.Load(); got != 0 { |
| | | t.Fatalf("StopAndGetSamples before startup samples = %d, want 0", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 0 { |
| | | t.Fatalf("recognizes before startup samples = %d, want 0", got) |
| | | } |
| | | |
| | | close(readSamplesReady) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return paster.pasted.Load() == "PrivateVoice AX POC" |
| | | }) |
| | | if got := recorder.stopAndGetSamples.Load(); got != 1 { |
| | | t.Fatalf("StopAndGetSamples after startup samples = %d, want 1", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 1 { |
| | | t.Fatalf("recognizes = %d, want 1", got) |
| | | } |
| | | |
| | | close(blockStart) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleaseWithoutStartupSamplesReportsAudioNotReady(t *testing.T) { |
| | | oldTimeout := holdStartupSampleWaitTimeout |
| | | oldPoll := holdStartupSamplePollInterval |
| | | holdStartupSampleWaitTimeout = 40 * time.Millisecond |
| | | holdStartupSamplePollInterval = 5 * time.Millisecond |
| | | defer func() { |
| | | holdStartupSampleWaitTimeout = oldTimeout |
| | | holdStartupSamplePollInterval = oldPoll |
| | | }() |
| | | |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | blockStart := make(chan struct{}) |
| | | recorder.startBlock = blockStart |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | paster := &fakeAppPaster{} |
| | | a.outputRouter = textoutput.NewRouter(paster) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | |
| | | listener.down.Store(false) |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return recorder.stops.Load() == 1 && |
| | | a.indicator.(*fakeOverlay).lastStatus.Load() == string(overlay.StatusError) |
| | | }) |
| | | if got := recorder.stopAndGetSamples.Load(); got != 0 { |
| | | t.Fatalf("StopAndGetSamples after audio-not-ready timeout = %d, want 0", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 0 { |
| | | t.Fatalf("recognizes after audio-not-ready timeout = %d, want 0", got) |
| | | } |
| | | if got := paster.pasted.Load(); got != nil { |
| | | t.Fatalf("pasted text after audio-not-ready timeout = %v, want nil", got) |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusError) { |
| | | t.Fatalf("indicator status = %q, want %q", got, overlay.StatusError) |
| | | } |
| | | |
| | | close(blockStart) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleaseWhileRecorderPermissionPendingDoesNotRecognize(t *testing.T) { |
| | | oldTimeout := holdStartupSampleWaitTimeout |
| | | oldPoll := holdStartupSamplePollInterval |
| | | holdStartupSampleWaitTimeout = 40 * time.Millisecond |
| | | holdStartupSamplePollInterval = 5 * time.Millisecond |
| | | defer func() { |
| | | holdStartupSampleWaitTimeout = oldTimeout |
| | | holdStartupSamplePollInterval = oldPoll |
| | | }() |
| | | |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | preStartBlock := make(chan struct{}) |
| | | recorder.preStartBlock = preStartBlock |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | paster := &fakeAppPaster{} |
| | | a.outputRouter = textoutput.NewRouter(paster) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | if got := recorder.starts.Load(); got != 0 { |
| | | t.Fatalf("recorder Start entered before permission pending release = %d, want 0", got) |
| | | } |
| | | |
| | | listener.down.Store(false) |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return recorder.stops.Load() == 1 && |
| | | a.indicator.(*fakeOverlay).lastStatus.Load() == string(overlay.StatusError) |
| | | }) |
| | | if got := recorder.stopAndGetSamples.Load(); got != 0 { |
| | | t.Fatalf("StopAndGetSamples after permission-pending audio-not-ready = %d, want 0", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 0 { |
| | | t.Fatalf("recognizes after permission-pending audio-not-ready = %d, want 0", got) |
| | | } |
| | | if got := paster.pasted.Load(); got != nil { |
| | | t.Fatalf("pasted text after permission-pending audio-not-ready = %v, want nil", got) |
| | | } |
| | | |
| | | close(preStartBlock) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | if got := recorder.starts.Load(); got != 1 { |
| | | t.Fatalf("recorder Start after permission resumes = %d, want 1", got) |
| | | } |
| | | if got := recorder.stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops after late permission success = %d, want 1", got) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureEscapeAfterConfirmDefersStopUntilRecorderStart(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | blockStart := make(chan struct{}) |
| | | recorder.startBlock = blockStart |
| | | captionCancelled := false |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | |
| | | a.mu.Lock() |
| | | a.liveCaptionCancel = func() { captionCancelled = true } |
| | | a.mu.Unlock() |
| | | listener.escapeDown.Store(true) |
| | | |
| | | done := make(chan struct{}) |
| | | go func() { |
| | | a.pollHotkey() |
| | | close(done) |
| | | }() |
| | | select { |
| | | case <-done: |
| | | case <-time.After(100 * time.Millisecond): |
| | | t.Fatal("Escape cancel should not wait for blocked recorder start") |
| | | } |
| | | |
| | | if got := recorder.stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops before start returned = %d, want 1", got) |
| | | } |
| | | if got := recorder.stopAndGetSamples.Load(); got != 0 { |
| | | t.Fatalf("StopAndGetSamples before recorder start returned = %d, want 0", got) |
| | | } |
| | | a.mu.Lock() |
| | | if !a.isHoldStopPending { |
| | | t.Fatal("Escape while recorder is starting should mark pending stop") |
| | | } |
| | | if a.liveCaptionCancel != nil { |
| | | t.Fatal("Escape while recorder is starting should clear live caption") |
| | | } |
| | | a.mu.Unlock() |
| | | if !captionCancelled { |
| | | t.Fatal("Escape while recorder is starting should cancel live caption") |
| | | } |
| | | |
| | | close(blockStart) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | |
| | | if got := recorder.stopAndGetSamples.Load(); got != 0 { |
| | | t.Fatalf("StopAndGetSamples after deferred Escape stop = %d, want 0", got) |
| | | } |
| | | if got := recorder.stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops = %d, want 1", got) |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusCancelled) { |
| | | t.Fatalf("indicator status = %q, want %q", got, overlay.StatusCancelled) |
| | | } |
| | | } |
| | | |
| | | func TestPollHotkeyIgnoresUnsetHotkey(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | a.cfg.HotkeyVK = config.UnsetHotkeyVK |
| | | |
| | | a.pollHotkey() |
| | | |
| | | if got := listener.keyChecks.Load(); got != 0 { |
| | | t.Fatalf("IsKeyDown calls for unset hotkey = %d, want 0", got) |
| | | } |
| | | if a.hotkeyActive { |
| | | t.Fatal("unset hotkey must not become active") |
| | | } |
| | | if a.isRecording || a.isFreetalking { |
| | | t.Fatal("unset hotkey must not start recording") |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureEscapeWhileRecorderPermissionPendingStopsLateStart(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | preStartBlock := make(chan struct{}) |
| | | recorder.preStartBlock = preStartBlock |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | if got := recorder.starts.Load(); got != 0 { |
| | | t.Fatalf("recorder Start entered before Escape during permission pending = %d, want 0", got) |
| | | } |
| | | |
| | | listener.escapeDown.Store(true) |
| | | done := make(chan struct{}) |
| | | go func() { |
| | | a.pollHotkey() |
| | | close(done) |
| | | }() |
| | | select { |
| | | case <-done: |
| | | case <-time.After(100 * time.Millisecond): |
| | | t.Fatal("Escape during permission pending should not wait for recorder Start") |
| | | } |
| | | if got := recorder.stops.Load(); got != 1 { |
| | | t.Fatalf("recorder Stop during permission-pending Escape = %d, want 1", got) |
| | | } |
| | | if got := recorder.stopAndGetSamples.Load(); got != 0 { |
| | | t.Fatalf("StopAndGetSamples during permission-pending Escape = %d, want 0", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 0 { |
| | | t.Fatalf("recognizes during permission-pending Escape = %d, want 0", got) |
| | | } |
| | | |
| | | close(preStartBlock) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | if got := recorder.stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops after late permission success = %d, want 1", got) |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusCancelled) { |
| | | t.Fatalf("indicator status = %q, want %q", got, overlay.StatusCancelled) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureCombinationWhileRecorderPermissionPendingStopsLateStart(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | preStartBlock := make(chan struct{}) |
| | | recorder.preStartBlock = preStartBlock |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | listener.otherSince.Store(true) |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isCombination && !a.isRecording && !a.isHoldRecordingPending && a.isHoldStopPending |
| | | }) |
| | | if got := recorder.stops.Load(); got != 1 { |
| | | t.Fatalf("recorder Stop during permission-pending combination cancel = %d, want 1", got) |
| | | } |
| | | if got := recorder.stopAndGetSamples.Load(); got != 0 { |
| | | t.Fatalf("StopAndGetSamples during permission-pending combination cancel = %d, want 0", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 0 { |
| | | t.Fatalf("recognizes during permission-pending combination cancel = %d, want 0", got) |
| | | } |
| | | |
| | | close(preStartBlock) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && !a.isHoldStopPending |
| | | }) |
| | | if got := recorder.stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops after late combination permission success = %d, want 1", got) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleaseAfterRecorderStartRunsRecognitionAndOutput(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} |
| | | recorder.hasVoice.Store(true) |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | paster := &fakeAppPaster{} |
| | | a.outputRouter = textoutput.NewRouter(paster) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && !a.isHoldRecorderStarting |
| | | }) |
| | | |
| | | listener.down.Store(false) |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return paster.pasted.Load() == "PrivateVoice AX POC" |
| | | }) |
| | | |
| | | if got := recorder.stopAndGetSamples.Load(); got != 1 { |
| | | t.Fatalf("StopAndGetSamples = %d, want 1", got) |
| | | } |
| | | if got := eng.recognizes.Load(); got != 1 { |
| | | t.Fatalf("recognizes = %d, want 1", got) |
| | | } |
| | | if got := recorder.stops.Load(); got != 0 { |
| | | t.Fatalf("recorder Stop = %d, want 0 for normal recognition path", got) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleasePipelineSuppressesLateRecorderStartError(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | listener := a.hk.(*fakeHotkeyListener) |
| | | listener.down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | blockStart := make(chan struct{}) |
| | | recorder.startBlock = blockStart |
| | | recorder.startErr = errors.New("start failed") |
| | | recorder.samples = []float32{0.2, 0.1, -0.1, -0.2} |
| | | recorder.hasVoice.Store(true) |
| | | eng := &recognizingHoldEngine{text: "PrivateVoice AX POC"} |
| | | a.replaceEngine(eng) |
| | | paster := &fakeAppPaster{} |
| | | a.outputRouter = textoutput.NewRouter(paster) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting |
| | | }) |
| | | |
| | | listener.down.Store(false) |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | return paster.pasted.Load() == "PrivateVoice AX POC" |
| | | }) |
| | | close(blockStart) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isHoldRecorderStarting |
| | | }) |
| | | |
| | | if got := eng.recognizes.Load(); got != 1 { |
| | | t.Fatalf("recognizes = %d, want 1", got) |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusDone) { |
| | | t.Fatalf("late recorder start error overwrote indicator status = %q, want %q", got, overlay.StatusDone) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureRecorderStartLateErrorCleansConfirmedState(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | a.hk.(*fakeHotkeyListener).down.Store(true) |
| | | recorder := a.recorder.(*fakeAppRecorder) |
| | | blockStart := make(chan struct{}) |
| | | recorder.startBlock = blockStart |
| | | recorder.startErr = errors.New("start failed") |
| | | captionCancelled := false |
| | | a.liveCaptionCancel = func() { captionCancelled = true } |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isRecording && !a.isHoldRecordingPending && a.isHoldRecorderStarting && a.liveCaptionCancel != nil |
| | | }) |
| | | |
| | | close(blockStart) |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecorderStarting && a.liveCaptionCancel == nil |
| | | }) |
| | | |
| | | if !captionCancelled { |
| | | t.Fatal("late recorder start error should cancel live caption") |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got != string(overlay.StatusError) { |
| | | t.Fatalf("indicator status = %q, want %q", got, overlay.StatusError) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureReleaseBeforeActivationCancelsTimer(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | a.hk.(*fakeHotkeyListener).down.Store(true) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.hk.(*fakeHotkeyListener).down.Store(false) |
| | | a.pollHoldHotkeyLocked(false, true) |
| | | a.mu.Unlock() |
| | | |
| | | time.Sleep(holdActivationDelay + 50*time.Millisecond) |
| | | |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | if a.isRecording || a.isHoldRecordingPending { |
| | | t.Fatalf("recording=%t pending=%t, want both false after early release", a.isRecording, a.isHoldRecordingPending) |
| | | } |
| | | if got := a.recorder.(*fakeAppRecorder).stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops = %d, want 1", got) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureTimerCancelsWhenKeyReleasedBeforePoll(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | a.hk.(*fakeHotkeyListener).down.Store(true) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | a.hk.(*fakeHotkeyListener).down.Store(false) |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return !a.isRecording && !a.isHoldRecordingPending |
| | | }) |
| | | |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | if a.hotkeyActive != true { |
| | | t.Fatal("hotkeyActive should remain true until release poll handles the key-up") |
| | | } |
| | | if got := a.recorder.(*fakeAppRecorder).stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops = %d, want 1", got) |
| | | } |
| | | if got := a.indicator.(*fakeOverlay).lastStatus.Load(); got == string(overlay.StatusRecording) { |
| | | t.Fatalf("indicator status = %q, should not confirm recording after physical release", got) |
| | | } |
| | | } |
| | | |
| | | func TestHoldPreCaptureCombinationBeforeActivationCancels(t *testing.T) { |
| | | a, cleanup := newHoldPreCaptureTestApp() |
| | | defer cleanup() |
| | | a.hk.(*fakeHotkeyListener).down.Store(true) |
| | | |
| | | a.mu.Lock() |
| | | a.pollHoldHotkeyLocked(true, true) |
| | | a.mu.Unlock() |
| | | |
| | | a.hk.(*fakeHotkeyListener).otherSince.Store(true) |
| | | |
| | | waitForCondition(t, time.Second, func() bool { |
| | | a.mu.Lock() |
| | | defer a.mu.Unlock() |
| | | return a.isCombination && !a.isRecording && !a.isHoldRecordingPending |
| | | }) |
| | | |
| | | if got := a.recorder.(*fakeAppRecorder).stops.Load(); got != 1 { |
| | | t.Fatalf("recorder stops = %d, want 1", got) |
| | | } |
| | | } |
| | | |
| | | func TestStartRecordingIgnoredWhileStoppingRecording(t *testing.T) { |
| | | a := &App{isStoppingRecording: true} |
| | | |
| | |
| | | func (e holdPreCaptureTestEngine) HoldPreCaptureEnabled() bool { |
| | | return e.enabled |
| | | } |
| | | |
| | | func newHoldPreCaptureTestApp() (*App, context.CancelFunc) { |
| | | ctx, cancel := context.WithCancel(context.Background()) |
| | | a := &App{ |
| | | ctx: ctx, |
| | | cancel: cancel, |
| | | cfg: &config.Config{HotkeyVK: 0x5C, HotkeyMode: config.HotkeyModeHold}, |
| | | recorder: &fakeAppRecorder{}, |
| | | hk: &fakeHotkeyListener{}, |
| | | indicator: &fakeOverlay{}, |
| | | history: history.New(), |
| | | userdict: userdict.New(), |
| | | } |
| | | a.replaceEngine(holdPreCaptureTestEngine{enabled: true}) |
| | | a.holdPreCaptureEnabled.Store(true) |
| | | return a, cancel |
| | | } |
| | | |
| | | type fakeAppRecorder struct { |
| | | starts atomic.Int32 |
| | | stops atomic.Int32 |
| | | stopAndGetSamples atomic.Int32 |
| | | hasVoice atomic.Bool |
| | | samples []float32 |
| | | preStartBlock <-chan struct{} |
| | | startBlock <-chan struct{} |
| | | readSamplesBlock <-chan struct{} |
| | | startErr error |
| | | } |
| | | |
| | | func (r *fakeAppRecorder) OnVolume(func(float64)) {} |
| | | func (r *fakeAppRecorder) OnDeviceChange(func(string)) {} |
| | | func (r *fakeAppRecorder) Start() error { |
| | | if r.preStartBlock != nil { |
| | | <-r.preStartBlock |
| | | } |
| | | r.starts.Add(1) |
| | | if r.startBlock != nil { |
| | | <-r.startBlock |
| | | } |
| | | return r.startErr |
| | | } |
| | | func (r *fakeAppRecorder) Stop() { r.stops.Add(1) } |
| | | func (r *fakeAppRecorder) StopAndGetSamples() []float32 { |
| | | r.stopAndGetSamples.Add(1) |
| | | return append([]float32(nil), r.samples...) |
| | | } |
| | | func (r *fakeAppRecorder) HasVoiceActivity() bool { return r.hasVoice.Load() } |
| | | func (r *fakeAppRecorder) ReadSamplesSince(offset int) ([]float32, int) { |
| | | if r.readSamplesBlock != nil { |
| | | select { |
| | | case <-r.readSamplesBlock: |
| | | default: |
| | | return nil, 0 |
| | | } |
| | | } |
| | | total := len(r.samples) |
| | | if offset < 0 { |
| | | offset = 0 |
| | | } |
| | | if offset > total { |
| | | offset = total |
| | | } |
| | | return append([]float32(nil), r.samples[offset:]...), total |
| | | } |
| | | func (r *fakeAppRecorder) Close() {} |
| | | |
| | | type recognizingHoldEngine struct { |
| | | text string |
| | | recognizes atomic.Int32 |
| | | } |
| | | |
| | | func (e *recognizingHoldEngine) Recognize([]float32) (string, error) { |
| | | e.recognizes.Add(1) |
| | | return e.text, nil |
| | | } |
| | | func (e *recognizingHoldEngine) HardwareInfo() string { return "test" } |
| | | func (e *recognizingHoldEngine) Close() {} |
| | | func (e *recognizingHoldEngine) HoldPreCaptureEnabled() bool { return true } |
| | | |
| | | type fakeAppPaster struct { |
| | | pasted atomic.Value |
| | | typed atomic.Value |
| | | } |
| | | |
| | | func (p *fakeAppPaster) Paste(text string, keepClipboard bool) error { |
| | | p.pasted.Store(text) |
| | | return nil |
| | | } |
| | | |
| | | func (p *fakeAppPaster) TypeText(text string) error { |
| | | p.typed.Store(text) |
| | | return nil |
| | | } |
| | | |
| | | type fakeHotkeyListener struct { |
| | | down atomic.Bool |
| | | escapeDown atomic.Bool |
| | | otherSince atomic.Bool |
| | | keyChecks atomic.Int32 |
| | | } |
| | | |
| | | func (h *fakeHotkeyListener) IsKeyDown(key int) bool { |
| | | h.keyChecks.Add(1) |
| | | if key == 0x1B { |
| | | return h.escapeDown.Load() |
| | | } |
| | | return h.down.Load() |
| | | } |
| | | func (h *fakeHotkeyListener) IsAnyOtherKeyPressed(int) bool { return false } |
| | | func (h *fakeHotkeyListener) IsAnyOtherKeyPressedSince(int, time.Time) bool { |
| | | return h.otherSince.Load() |
| | | } |
| | | func (h *fakeHotkeyListener) Close() {} |
| | | |
| | | type fakeOverlay struct { |
| | | lastStatus atomic.Value |
| | | } |
| | | |
| | | func (o *fakeOverlay) Show() {} |
| | | func (o *fakeOverlay) Hide() {} |
| | | func (o *fakeOverlay) SetStatus(status overlay.Status, text string) { |
| | | o.lastStatus.Store(string(status)) |
| | | } |
| | | func (o *fakeOverlay) SetVolume(float64) {} |
| | | func (o *fakeOverlay) SetPosition(int, int) {} |
| | | func (o *fakeOverlay) GetPosition() (int, int) { return 0, 0 } |
| | | func (o *fakeOverlay) Size() (int, int) { return 170, 48 } |
| | | func (o *fakeOverlay) OnDragged(func(int, int)) {} |
| | | func (o *fakeOverlay) Close() {} |
| | | func (o *fakeOverlay) AutoPosition() {} |