Ariver
2026-07-11 015f1089ea875a153dd2a18ef42c25145d4868f5
C1.source/privatevoice.src/internal/audio/recorder_test.go
@@ -1,6 +1,25 @@
package audio
import "testing"
import (
   "errors"
   "os"
   "sync"
   "sync/atomic"
   "testing"
   "time"
   "github.com/gen2brain/malgo"
)
func TestMain(m *testing.M) {
   oldEnsure := ensureMicrophoneAuthorization
   ensureMicrophoneAuthorization = func() microphoneAuthorization {
      return microphoneAuthorization{granted: true, state: "granted"}
   }
   code := m.Run()
   ensureMicrophoneAuthorization = oldEnsure
   os.Exit(code)
}
func TestReadSamplesSinceReturnsOnlyNewSamples(t *testing.T) {
   r := &Recorder{
@@ -57,6 +76,388 @@
   }
}
func TestStartWithoutMicrophonePermissionDoesNotInitializeDevice(t *testing.T) {
   oldEnsure := ensureMicrophoneAuthorization
   ensureMicrophoneAuthorization = func() microphoneAuthorization {
      return microphoneAuthorization{granted: false, state: "denied"}
   }
   defer func() {
      ensureMicrophoneAuthorization = oldEnsure
   }()
   device := newFakeRecorderDevice()
   ctx := &fakeRecorderContext{device: device}
   r := &Recorder{ctx: ctx}
   err := r.Start()
   if err == nil {
      t.Fatal("Start returned nil without microphone permission")
   }
   if ctx.initEntered.Load() {
      t.Fatal("initDevice should not run without microphone permission")
   }
   if device.startEntered.Load() {
      t.Fatal("device Start should not run without microphone permission")
   }
   r.mu.Lock()
   defer r.mu.Unlock()
   if r.startRequested || r.isStarting || r.isRecording || r.stopAfterStart || r.device != nil {
      t.Fatalf("recorder state leaked after microphone denial: startRequested=%t isStarting=%t isRecording=%t stopAfterStart=%t deviceNil=%t",
         r.startRequested, r.isStarting, r.isRecording, r.stopAfterStart, r.device == nil)
   }
}
func TestStopWhileMicrophoneAuthorizationPendingPreventsNativeStart(t *testing.T) {
   oldEnsure := ensureMicrophoneAuthorization
   entered := make(chan struct{})
   allow := make(chan struct{})
   var enteredOnce sync.Once
   ensureMicrophoneAuthorization = func() microphoneAuthorization {
      enteredOnce.Do(func() { close(entered) })
      <-allow
      return microphoneAuthorization{granted: true, state: "granted"}
   }
   defer func() {
      ensureMicrophoneAuthorization = oldEnsure
   }()
   device := newFakeRecorderDevice()
   ctx := &fakeRecorderContext{device: device}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   select {
   case <-entered:
   case <-time.After(time.Second):
      t.Fatal("microphone authorization was not entered")
   }
   r.Stop()
   close(allow)
   if err := <-startDone; err != nil {
      t.Fatalf("Start returned error = %v", err)
   }
   if ctx.initEntered.Load() {
      t.Fatal("initDevice should not run after Stop during microphone authorization")
   }
   if device.startEntered.Load() {
      t.Fatal("device Start should not run after Stop during microphone authorization")
   }
   r.mu.Lock()
   defer r.mu.Unlock()
   if r.startRequested || r.isStarting || r.isRecording || r.stopAfterStart || r.device != nil {
      t.Fatalf("recorder state leaked after pending authorization stop: startRequested=%t isStarting=%t isRecording=%t stopAfterStart=%t deviceNil=%t",
         r.startRequested, r.isStarting, r.isRecording, r.stopAfterStart, r.device == nil)
   }
}
func TestStopAndGetSamplesWhileMicrophoneAuthorizationPendingPreventsNativeStart(t *testing.T) {
   oldEnsure := ensureMicrophoneAuthorization
   entered := make(chan struct{})
   allow := make(chan struct{})
   var enteredOnce sync.Once
   ensureMicrophoneAuthorization = func() microphoneAuthorization {
      enteredOnce.Do(func() { close(entered) })
      <-allow
      return microphoneAuthorization{granted: true, state: "granted"}
   }
   defer func() {
      ensureMicrophoneAuthorization = oldEnsure
   }()
   device := newFakeRecorderDevice()
   ctx := &fakeRecorderContext{device: device}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   select {
   case <-entered:
   case <-time.After(time.Second):
      t.Fatal("microphone authorization was not entered")
   }
   samples := r.StopAndGetSamples()
   if len(samples) != 0 {
      t.Fatalf("samples while microphone authorization pending = %d, want 0", len(samples))
   }
   close(allow)
   if err := <-startDone; err != nil {
      t.Fatalf("Start returned error = %v", err)
   }
   if ctx.initEntered.Load() {
      t.Fatal("initDevice should not run after StopAndGetSamples during microphone authorization")
   }
   if device.startEntered.Load() {
      t.Fatal("device Start should not run after StopAndGetSamples during microphone authorization")
   }
   r.mu.Lock()
   defer r.mu.Unlock()
   if r.startRequested || r.isStarting || r.isRecording || r.stopAfterStart || r.device != nil {
      t.Fatalf("recorder state leaked after pending authorization samples stop: startRequested=%t isStarting=%t isRecording=%t stopAfterStart=%t deviceNil=%t",
         r.startRequested, r.isStarting, r.isRecording, r.stopAfterStart, r.device == nil)
   }
}
func TestCloseWaitsForBlockedStartBeforeFreeingContext(t *testing.T) {
   device := newFakeRecorderDevice()
   ctx := &fakeRecorderContext{device: device}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   waitForRecorderTest(t, func() bool { return device.startEntered.Load() })
   closeDone := make(chan struct{})
   go func() {
      r.Close()
      close(closeDone)
   }()
   select {
   case <-closeDone:
      t.Fatal("Close returned while recorder Start was still blocked")
   case <-time.After(50 * time.Millisecond):
   }
   if got := ctx.freeCount.Load(); got != 0 {
      t.Fatalf("context freed before Start returned = %d, want 0", got)
   }
   if got := device.uninitCount.Load(); got != 0 {
      t.Fatalf("device uninitialized before Start returned = %d, want 0", got)
   }
   close(device.allowStart)
   if err := <-startDone; err == nil {
      t.Fatal("Start returned nil after Close was requested during startup")
   }
   waitForRecorderTest(t, func() bool {
      select {
      case <-closeDone:
         return true
      default:
         return false
      }
   })
   if got := ctx.freeCount.Load(); got != 1 {
      t.Fatalf("context free count = %d, want 1", got)
   }
   if got := device.stopCount.Load(); got != 1 {
      t.Fatalf("device stop count = %d, want 1", got)
   }
   if got := device.uninitCount.Load(); got != 1 {
      t.Fatalf("device uninit count = %d, want 1", got)
   }
}
func TestStopWhileStartBlockedDefersDeviceCleanup(t *testing.T) {
   device := newFakeRecorderDevice()
   ctx := &fakeRecorderContext{device: device}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   waitForRecorderTest(t, func() bool { return device.startEntered.Load() })
   stopDone := make(chan struct{})
   go func() {
      r.Stop()
      close(stopDone)
   }()
   select {
   case <-stopDone:
   case <-time.After(time.Second):
      t.Fatal("Stop should return while recorder Start is still blocked")
   }
   if got := device.stopCount.Load(); got != 0 {
      t.Fatalf("device stopped before Start returned = %d, want 0", got)
   }
   if got := device.uninitCount.Load(); got != 0 {
      t.Fatalf("device uninitialized before Start returned = %d, want 0", got)
   }
   close(device.allowStart)
   if err := <-startDone; err != nil {
      t.Fatalf("Start returned error = %v", err)
   }
   waitForRecorderTest(t, func() bool { return device.uninitCount.Load() == 1 })
   if got := device.stopCount.Load(); got != 1 {
      t.Fatalf("device stop count = %d, want 1", got)
   }
}
func TestStopAndGetSamplesWhileStartBlockedReturnsBufferedPCM(t *testing.T) {
   device := newFakeRecorderDevice()
   ctx := &fakeRecorderContext{device: device}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   waitForRecorderTest(t, func() bool { return device.startEntered.Load() })
   r.onData(pcm16LE(16384, -32768))
   samples := r.StopAndGetSamples()
   if len(samples) != 2 {
      t.Fatalf("samples len = %d, want 2", len(samples))
   }
   if samples[0] != 0.5 || samples[1] != -1 {
      t.Fatalf("samples = %#v, want [0.5 -1]", samples)
   }
   if got := device.stopCount.Load(); got != 0 {
      t.Fatalf("device stopped before Start returned = %d, want 0", got)
   }
   if got := device.uninitCount.Load(); got != 0 {
      t.Fatalf("device uninitialized before Start returned = %d, want 0", got)
   }
   close(device.allowStart)
   if err := <-startDone; err != nil {
      t.Fatalf("Start returned error = %v", err)
   }
   if got := device.stopCount.Load(); got != 1 {
      t.Fatalf("device stop count after Start returned = %d, want 1", got)
   }
   if got := device.uninitCount.Load(); got != 1 {
      t.Fatalf("device uninit count after Start returned = %d, want 1", got)
   }
}
func TestStopAndGetSamplesBeforeDevicePublishDefersIntentAndCleansInitializedDevice(t *testing.T) {
   device := newFakeRecorderDevice()
   initBlock := make(chan struct{})
   ctx := &fakeRecorderContext{device: device, initBlock: initBlock}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   waitForRecorderTest(t, func() bool { return ctx.initEntered.Load() })
   samples := r.StopAndGetSamples()
   if len(samples) != 0 {
      t.Fatalf("samples len before device publish = %d, want 0", len(samples))
   }
   if device.startEntered.Load() {
      t.Fatal("device Start should not be entered while initDevice is blocked")
   }
   if got := device.stopCount.Load(); got != 0 {
      t.Fatalf("device stopped before it was published = %d, want 0", got)
   }
   if got := device.uninitCount.Load(); got != 0 {
      t.Fatalf("device uninit before initDevice returned = %d, want 0", got)
   }
   close(initBlock)
   if err := <-startDone; err != nil {
      t.Fatalf("Start returned error = %v", err)
   }
   if device.startEntered.Load() {
      t.Fatal("device Start should not run after stop intent arrived before publish")
   }
   if got := device.stopCount.Load(); got != 0 {
      t.Fatalf("device stop count = %d, want 0 for never-started device", got)
   }
   if got := device.uninitCount.Load(); got != 1 {
      t.Fatalf("device uninit count = %d, want 1", got)
   }
   r.mu.Lock()
   defer r.mu.Unlock()
   if r.startRequested || r.isStarting || r.isRecording || r.stopAfterStart || r.device != nil {
      t.Fatalf("recorder state leaked after pre-publish stop: startRequested=%t isStarting=%t isRecording=%t stopAfterStart=%t deviceNil=%t",
         r.startRequested, r.isStarting, r.isRecording, r.stopAfterStart, r.device == nil)
   }
}
func TestStopBeforeDevicePublishDefersIntentAndCleansInitializedDevice(t *testing.T) {
   device := newFakeRecorderDevice()
   initBlock := make(chan struct{})
   ctx := &fakeRecorderContext{device: device, initBlock: initBlock}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   waitForRecorderTest(t, func() bool { return ctx.initEntered.Load() })
   r.Stop()
   if device.startEntered.Load() {
      t.Fatal("device Start should not be entered while initDevice is blocked")
   }
   if got := device.stopCount.Load(); got != 0 {
      t.Fatalf("device stopped before it was published = %d, want 0", got)
   }
   if got := device.uninitCount.Load(); got != 0 {
      t.Fatalf("device uninit before initDevice returned = %d, want 0", got)
   }
   close(initBlock)
   if err := <-startDone; err != nil {
      t.Fatalf("Start returned error = %v", err)
   }
   if device.startEntered.Load() {
      t.Fatal("device Start should not run after stop intent arrived before publish")
   }
   if got := device.stopCount.Load(); got != 0 {
      t.Fatalf("device stop count = %d, want 0 for never-started device", got)
   }
   if got := device.uninitCount.Load(); got != 1 {
      t.Fatalf("device uninit count = %d, want 1", got)
   }
}
func TestStopWhileStartBlockedLetsStartErrorCleanup(t *testing.T) {
   startErr := errors.New("start failed")
   device := newFakeRecorderDevice()
   device.startErr = startErr
   ctx := &fakeRecorderContext{device: device}
   r := &Recorder{ctx: ctx}
   startDone := make(chan error, 1)
   go func() {
      startDone <- r.Start()
   }()
   waitForRecorderTest(t, func() bool { return device.startEntered.Load() })
   stopDone := make(chan struct{})
   go func() {
      r.Stop()
      close(stopDone)
   }()
   select {
   case <-stopDone:
   case <-time.After(time.Second):
      t.Fatal("Stop should return while recorder Start is still blocked")
   }
   if got := device.uninitCount.Load(); got != 0 {
      t.Fatalf("device uninit before Start error returned = %d, want 0", got)
   }
   close(device.allowStart)
   if err := <-startDone; !errors.Is(err, startErr) {
      t.Fatalf("Start error = %v, want %v", err, startErr)
   }
   if got := device.uninitCount.Load(); got != 1 {
      t.Fatalf("device uninit count = %d, want 1", got)
   }
}
func pcm16LE(values ...int16) []byte {
   buf := make([]byte, len(values)*2)
   for i, value := range values {
@@ -65,3 +466,72 @@
   }
   return buf
}
type fakeRecorderContext struct {
   device      *fakeRecorderDevice
   initErr     error
   initBlock   <-chan struct{}
   initEntered atomic.Bool
   freeCount   atomic.Int32
}
func (c *fakeRecorderContext) devices(malgo.DeviceType) ([]malgo.DeviceInfo, error) {
   return nil, nil
}
func (c *fakeRecorderContext) initDevice(malgo.DeviceConfig, malgo.DeviceCallbacks) (recorderDevice, error) {
   c.initEntered.Store(true)
   if c.initBlock != nil {
      <-c.initBlock
   }
   if c.initErr != nil {
      return nil, c.initErr
   }
   return c.device, nil
}
func (c *fakeRecorderContext) free() {
   c.freeCount.Add(1)
}
type fakeRecorderDevice struct {
   startEntered atomic.Bool
   startOnce    sync.Once
   allowStart   chan struct{}
   startErr     error
   stopCount    atomic.Int32
   uninitCount  atomic.Int32
}
func newFakeRecorderDevice() *fakeRecorderDevice {
   return &fakeRecorderDevice{allowStart: make(chan struct{})}
}
func (d *fakeRecorderDevice) Start() error {
   d.startOnce.Do(func() {
      d.startEntered.Store(true)
   })
   <-d.allowStart
   return d.startErr
}
func (d *fakeRecorderDevice) Stop() error {
   d.stopCount.Add(1)
   return nil
}
func (d *fakeRecorderDevice) Uninit() {
   d.uninitCount.Add(1)
}
func waitForRecorderTest(t *testing.T, cond func() bool) {
   t.Helper()
   deadline := time.Now().Add(2 * time.Second)
   for time.Now().Before(deadline) {
      if cond() {
         return
      }
      time.Sleep(10 * time.Millisecond)
   }
   t.Fatal("condition not met before timeout")
}