add tests

This commit is contained in:
LeonardoTrapani
2025-08-16 17:46:50 +02:00
parent feda62a7d4
commit 0b64a71afd
9 changed files with 2814 additions and 12 deletions
+557
View File
@@ -0,0 +1,557 @@
package transcriber
import (
"context"
"testing"
"time"
"github.com/leonardotrapani/hyprvoice/internal/recording"
)
func TestDefaultConfig(t *testing.T) {
config := DefaultConfig()
t.Run("default values", func(t *testing.T) {
if config.Provider != "openai" {
t.Errorf("default provider should be openai, got %s", config.Provider)
}
if config.Language != "it" {
t.Errorf("default language should be it, got %s", config.Language)
}
if config.ChunkSize != 16384 {
t.Errorf("default chunk size should be 16384, got %d", config.ChunkSize)
}
if config.BufferTime != 2*time.Second {
t.Errorf("default buffer time should be 2s, got %v", config.BufferTime)
}
if config.Model != "whisper-1" {
t.Errorf("default model should be whisper-1, got %s", config.Model)
}
if config.APIKey != "" {
t.Errorf("default API key should be empty, got %s", config.APIKey)
}
})
}
func TestNewTranscriber(t *testing.T) {
t.Run("openai provider with API key", func(t *testing.T) {
config := Config{
Provider: "openai",
APIKey: "test-api-key",
}
transcriber, err := NewTranscriber(config)
if err != nil {
t.Fatalf("NewTranscriber failed: %v", err)
}
if transcriber == nil {
t.Fatal("transcriber should not be nil")
}
})
t.Run("openai provider without API key", func(t *testing.T) {
config := Config{
Provider: "openai",
APIKey: "",
}
_, err := NewTranscriber(config)
if err == nil {
t.Error("NewTranscriber should fail without API key")
}
expectedMsg := "OpenAI API key required"
if err.Error() != expectedMsg {
t.Errorf("error message should be %q, got %q", expectedMsg, err.Error())
}
})
t.Run("unsupported provider", func(t *testing.T) {
config := Config{
Provider: "unsupported",
APIKey: "test-key",
}
_, err := NewTranscriber(config)
if err == nil {
t.Error("NewTranscriber should fail with unsupported provider")
}
expectedMsg := "unsupported provider: unsupported"
if err.Error() != expectedMsg {
t.Errorf("error message should be %q, got %q", expectedMsg, err.Error())
}
})
}
func TestOpenAITranscriberCreation(t *testing.T) {
config := Config{
Provider: "openai",
APIKey: "test-api-key",
Language: "en",
ChunkSize: 8192,
BufferTime: 1 * time.Second,
Model: "whisper-1",
}
transcriber := NewOpenAITranscriber(config)
t.Run("creation", func(t *testing.T) {
if transcriber == nil {
t.Fatal("OpenAI transcriber should not be nil")
}
if transcriber.config.APIKey != config.APIKey {
t.Error("transcriber should store the provided config")
}
if transcriber.client == nil {
t.Error("transcriber should have OpenAI client")
}
if transcriber.buffer == nil {
t.Error("transcriber should have audio buffer")
}
})
t.Run("initial state", func(t *testing.T) {
if transcriber.transcribing {
t.Error("transcriber should not be transcribing initially")
}
text, err := transcriber.GetTranscription()
if err != nil {
t.Errorf("GetTranscription should not error initially: %v", err)
}
if text != "" {
t.Errorf("initial transcription should be empty, got %q", text)
}
})
}
func TestAudioBuffer(t *testing.T) {
config := Config{
ChunkSize: 1024,
}
buffer := &audioBuffer{
data: make([]byte, 0, config.ChunkSize*2),
maxSize: config.ChunkSize,
}
t.Run("initial state", func(t *testing.T) {
if buffer.hasData() {
t.Error("buffer should not have data initially")
}
if buffer.shouldFlush(time.Second) {
t.Error("buffer should not need flushing initially")
}
data := buffer.flush()
if len(data) != 0 {
t.Error("flushing empty buffer should return empty data")
}
})
t.Run("add frame", func(t *testing.T) {
frame := recording.AudioFrame{
Data: []byte("test audio data"),
Timestamp: time.Now(),
}
buffer.addFrame(frame)
if !buffer.hasData() {
t.Error("buffer should have data after adding frame")
}
if len(buffer.data) != len(frame.Data) {
t.Errorf("buffer data length should be %d, got %d", len(frame.Data), len(buffer.data))
}
// Check that data was copied correctly
for i, b := range buffer.data {
if b != frame.Data[i] {
t.Errorf("buffer data[%d] should be %d, got %d", i, frame.Data[i], b)
}
}
})
t.Run("flush buffer", func(t *testing.T) {
data := buffer.flush()
if len(data) == 0 {
t.Error("flush should return data")
}
if buffer.hasData() {
t.Error("buffer should be empty after flush")
}
// Flush again should return empty
data2 := buffer.flush()
if len(data2) != 0 {
t.Error("second flush should return empty data")
}
})
t.Run("buffer overflow", func(t *testing.T) {
// Fill buffer beyond maxSize
largeData := make([]byte, config.ChunkSize*3)
for i := range largeData {
largeData[i] = byte(i % 256)
}
frame := recording.AudioFrame{
Data: largeData,
Timestamp: time.Now(),
}
buffer.addFrame(frame)
// Buffer should be trimmed to maxSize
if len(buffer.data) > config.ChunkSize {
t.Errorf("buffer should be trimmed to maxSize %d, got %d", config.ChunkSize, len(buffer.data))
}
})
t.Run("should flush conditions", func(t *testing.T) {
// Reset buffer for this test
buffer = &audioBuffer{
data: make([]byte, 0, config.ChunkSize*2),
maxSize: config.ChunkSize,
}
// Empty buffer should not flush
if buffer.shouldFlush(time.Second) {
t.Error("empty buffer should not need flushing")
}
// Add data
frame := recording.AudioFrame{
Data: make([]byte, 10),
Timestamp: time.Now(),
}
buffer.addFrame(frame)
// Fresh data should not flush immediately
if buffer.shouldFlush(time.Second) {
t.Error("fresh data should not need flushing immediately")
}
// Old data should flush
buffer.lastAdd = time.Now().Add(-2 * time.Second)
if !buffer.shouldFlush(time.Second) {
t.Error("old data should need flushing")
}
// Full buffer should flush regardless of time
buffer.data = make([]byte, buffer.maxSize)
buffer.lastAdd = time.Now()
if !buffer.shouldFlush(time.Hour) {
t.Error("full buffer should need flushing")
}
})
}
func TestOpenAITranscriberLifecycle(t *testing.T) {
config := Config{
Provider: "openai",
APIKey: "test-api-key",
Language: "en",
ChunkSize: 1024,
BufferTime: 100 * time.Millisecond,
Model: "whisper-1",
}
transcriber := NewOpenAITranscriber(config)
t.Run("stop before start", func(t *testing.T) {
err := transcriber.Stop()
if err != nil {
t.Errorf("Stop should not error when not started: %v", err)
}
})
t.Run("start transcriber", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
frameCh := make(chan recording.AudioFrame, 10)
errCh, err := transcriber.Start(ctx, frameCh)
if err != nil {
t.Fatalf("Start failed: %v", err)
}
if errCh == nil {
t.Fatal("error channel should not be nil")
}
// Should be transcribing now
if !transcriber.transcribing {
t.Error("transcriber should be marked as transcribing")
}
// Stop transcriber
err = transcriber.Stop()
if err != nil {
t.Errorf("Stop failed: %v", err)
}
// Should not be transcribing anymore
if transcriber.transcribing {
t.Error("transcriber should not be marked as transcribing after stop")
}
})
t.Run("double start", func(t *testing.T) {
ctx := context.Background()
frameCh := make(chan recording.AudioFrame, 10)
// Mark as transcribing manually
transcriber.transcribing = true
defer func() { transcriber.transcribing = false }()
_, err := transcriber.Start(ctx, frameCh)
if err == nil {
t.Error("Start should fail when already transcribing")
}
expectedMsg := "transcriber: already transcribing"
if err.Error() != expectedMsg {
t.Errorf("error message should be %q, got %q", expectedMsg, err.Error())
}
})
}
func TestConvertToWAV(t *testing.T) {
config := Config{
Provider: "openai",
APIKey: "test-api-key",
}
transcriber := NewOpenAITranscriber(config)
t.Run("convert empty audio", func(t *testing.T) {
rawAudio := []byte{}
wavData, err := transcriber.convertToWAV(rawAudio)
if err != nil {
t.Fatalf("convertToWAV failed: %v", err)
}
// WAV header is 44 bytes minimum
if len(wavData) < 44 {
t.Errorf("WAV data should be at least 44 bytes, got %d", len(wavData))
}
// Check WAV header magic
if string(wavData[0:4]) != "RIFF" {
t.Error("WAV should start with RIFF")
}
if string(wavData[8:12]) != "WAVE" {
t.Error("WAV should contain WAVE identifier")
}
})
t.Run("convert audio data", func(t *testing.T) {
rawAudio := make([]byte, 1024) // 1KB of audio data
for i := range rawAudio {
rawAudio[i] = byte(i % 256)
}
wavData, err := transcriber.convertToWAV(rawAudio)
if err != nil {
t.Fatalf("convertToWAV failed: %v", err)
}
expectedSize := 44 + len(rawAudio) // Header + data
if len(wavData) != expectedSize {
t.Errorf("WAV data should be %d bytes, got %d", expectedSize, len(wavData))
}
// Check that audio data is at the end
audioDataStart := len(wavData) - len(rawAudio)
for i, b := range rawAudio {
if wavData[audioDataStart+i] != b {
t.Errorf("audio data[%d] should be %d, got %d", i, b, wavData[audioDataStart+i])
}
}
})
t.Run("WAV header validation", func(t *testing.T) {
rawAudio := make([]byte, 16) // Small audio sample
wavData, err := transcriber.convertToWAV(rawAudio)
if err != nil {
t.Fatalf("convertToWAV failed: %v", err)
}
// Validate WAV header fields
tests := []struct {
offset int
expected string
name string
}{
{0, "RIFF", "RIFF identifier"},
{8, "WAVE", "WAVE identifier"},
{12, "fmt ", "format chunk identifier"},
{36, "data", "data chunk identifier"},
}
for _, tt := range tests {
if tt.offset+4 > len(wavData) {
t.Errorf("WAV data too short for %s", tt.name)
continue
}
actual := string(wavData[tt.offset : tt.offset+4])
if actual != tt.expected {
t.Errorf("%s should be %q, got %q", tt.name, tt.expected, actual)
}
}
})
}
func TestTranscriberConcurrency(t *testing.T) {
config := Config{
Provider: "openai",
APIKey: "test-api-key",
}
transcriber := NewOpenAITranscriber(config)
t.Run("concurrent GetTranscription calls", func(t *testing.T) {
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 100; j++ {
_, _ = transcriber.GetTranscription()
}
done <- true
}()
}
for i := 0; i < 10; i++ {
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for concurrent GetTranscription calls")
}
}
})
t.Run("concurrent Stop calls", func(t *testing.T) {
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() {
transcriber.Stop()
done <- true
}()
}
for i := 0; i < 10; i++ {
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for concurrent Stop calls")
}
}
})
}
func TestTranscriptionResult(t *testing.T) {
t.Run("transcription result creation", func(t *testing.T) {
text := "Hello, world!"
timestamp := time.Now()
isFinal := true
result := TranscriptionResult{
Text: text,
Timestamp: timestamp,
IsFinal: isFinal,
}
if result.Text != text {
t.Errorf("text should be %q, got %q", text, result.Text)
}
if !result.Timestamp.Equal(timestamp) {
t.Errorf("timestamp should be %v, got %v", timestamp, result.Timestamp)
}
if result.IsFinal != isFinal {
t.Errorf("IsFinal should be %v, got %v", isFinal, result.IsFinal)
}
})
t.Run("empty transcription result", func(t *testing.T) {
result := TranscriptionResult{}
if result.Text != "" {
t.Error("empty result text should be empty string")
}
if !result.Timestamp.IsZero() {
t.Error("empty result timestamp should be zero")
}
if result.IsFinal {
t.Error("empty result should not be final")
}
})
}
func TestConfigValidation(t *testing.T) {
t.Run("various provider configurations", func(t *testing.T) {
tests := []struct {
name string
config Config
expectError bool
errorMsg string
}{
{
name: "valid openai config",
config: Config{
Provider: "openai",
APIKey: "sk-test-key",
Language: "en",
Model: "whisper-1",
},
expectError: false,
},
{
name: "openai without api key",
config: Config{
Provider: "openai",
APIKey: "",
},
expectError: true,
errorMsg: "OpenAI API key required",
},
{
name: "unknown provider",
config: Config{
Provider: "unknown",
APIKey: "test-key",
},
expectError: true,
errorMsg: "unsupported provider: unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewTranscriber(tt.config)
if tt.expectError {
if err == nil {
t.Errorf("expected error for config %+v", tt.config)
} else if err.Error() != tt.errorMsg {
t.Errorf("expected error message %q, got %q", tt.errorMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error for config %+v: %v", tt.config, err)
}
}
})
}
})
}