add tests
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPipelineStatus(t *testing.T) {
|
||||
p := New()
|
||||
|
||||
t.Run("initial status", func(t *testing.T) {
|
||||
status := p.Status()
|
||||
if status != "" && status != Idle {
|
||||
t.Errorf("initial status should be empty or Idle, got %s", status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("status changes", func(t *testing.T) {
|
||||
pipeline := p.(*pipeline)
|
||||
|
||||
pipeline.setStatus(Recording)
|
||||
if p.Status() != Recording {
|
||||
t.Errorf("status should be Recording, got %s", p.Status())
|
||||
}
|
||||
|
||||
pipeline.setStatus(Transcribing)
|
||||
if p.Status() != Transcribing {
|
||||
t.Errorf("status should be Transcribing, got %s", p.Status())
|
||||
}
|
||||
|
||||
pipeline.setStatus(Injecting)
|
||||
if p.Status() != Injecting {
|
||||
t.Errorf("status should be Injecting, got %s", p.Status())
|
||||
}
|
||||
|
||||
pipeline.setStatus(Idle)
|
||||
if p.Status() != Idle {
|
||||
t.Errorf("status should be Idle, got %s", p.Status())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPipelineChannels(t *testing.T) {
|
||||
p := New()
|
||||
|
||||
t.Run("action channel", func(t *testing.T) {
|
||||
actionCh := p.GetActionCh()
|
||||
if actionCh == nil {
|
||||
t.Error("action channel should not be nil")
|
||||
}
|
||||
|
||||
// Test non-blocking send
|
||||
select {
|
||||
case actionCh <- Inject:
|
||||
default:
|
||||
t.Error("action channel should accept at least one message")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error channel", func(t *testing.T) {
|
||||
errorCh := p.GetErrorCh()
|
||||
if errorCh == nil {
|
||||
t.Error("error channel should not be nil")
|
||||
}
|
||||
|
||||
// Should be empty initially
|
||||
select {
|
||||
case <-errorCh:
|
||||
t.Error("error channel should be empty initially")
|
||||
default:
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPipelineErrorHandling(t *testing.T) {
|
||||
p := New().(*pipeline)
|
||||
|
||||
t.Run("send error", func(t *testing.T) {
|
||||
testTitle := "Test Error"
|
||||
testMessage := "This is a test error"
|
||||
testErr := context.Canceled
|
||||
|
||||
p.sendError(testTitle, testMessage, testErr)
|
||||
|
||||
select {
|
||||
case pipelineErr := <-p.GetErrorCh():
|
||||
if pipelineErr.Title != testTitle {
|
||||
t.Errorf("error title should be %q, got %q", testTitle, pipelineErr.Title)
|
||||
}
|
||||
if pipelineErr.Message != testMessage {
|
||||
t.Errorf("error message should be %q, got %q", testMessage, pipelineErr.Message)
|
||||
}
|
||||
if pipelineErr.Err != testErr {
|
||||
t.Errorf("error should be %v, got %v", testErr, pipelineErr.Err)
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("should receive error on error channel")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error channel full", func(t *testing.T) {
|
||||
// Fill up the error channel (capacity is 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
p.sendError("Test", "Test message", nil)
|
||||
}
|
||||
|
||||
// This should not block (it should drop the error)
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
p.sendError("Overflow", "This should be dropped", nil)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Success - the send didn't block
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("sendError should not block when channel is full")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPipelineLifecycle(t *testing.T) {
|
||||
t.Run("multiple Run calls", func(t *testing.T) {
|
||||
p := New()
|
||||
ctx := context.Background()
|
||||
|
||||
// First Run should work
|
||||
p.Run(ctx)
|
||||
|
||||
// Second Run should be ignored (already running)
|
||||
p.Run(ctx)
|
||||
|
||||
// Stop should work
|
||||
p.Stop()
|
||||
})
|
||||
|
||||
t.Run("stop before run", func(t *testing.T) {
|
||||
p := New()
|
||||
|
||||
// Stop before run should not panic
|
||||
p.Stop()
|
||||
})
|
||||
|
||||
t.Run("multiple stops", func(t *testing.T) {
|
||||
p := New()
|
||||
ctx := context.Background()
|
||||
|
||||
p.Run(ctx)
|
||||
p.Stop()
|
||||
|
||||
// Second stop should not panic
|
||||
p.Stop()
|
||||
})
|
||||
}
|
||||
|
||||
func TestPipelineContextCancellation(t *testing.T) {
|
||||
t.Run("context cancelled during run", func(t *testing.T) {
|
||||
p := New()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Start pipeline
|
||||
p.Run(ctx)
|
||||
|
||||
// Cancel context immediately
|
||||
cancel()
|
||||
|
||||
// Wait for pipeline to stop
|
||||
p.Stop()
|
||||
|
||||
// Pipeline should be idle
|
||||
if p.Status() != Idle {
|
||||
t.Errorf("pipeline should be idle after context cancellation, got %s", p.Status())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("timeout context", func(t *testing.T) {
|
||||
p := New()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// Start pipeline
|
||||
p.Run(ctx)
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Pipeline should stop automatically
|
||||
p.Stop()
|
||||
|
||||
if p.Status() != Idle {
|
||||
t.Errorf("pipeline should be idle after timeout, got %s", p.Status())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPipelineConstants(t *testing.T) {
|
||||
t.Run("status constants", func(t *testing.T) {
|
||||
statuses := []Status{Idle, Recording, Transcribing, Injecting}
|
||||
for _, status := range statuses {
|
||||
if string(status) == "" {
|
||||
t.Errorf("status %v should have a string representation", status)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("action constants", func(t *testing.T) {
|
||||
actions := []Action{Inject}
|
||||
for _, action := range actions {
|
||||
if string(action) == "" {
|
||||
t.Errorf("action %v should have a string representation", action)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPipelineError(t *testing.T) {
|
||||
t.Run("pipeline error struct", func(t *testing.T) {
|
||||
title := "Test Title"
|
||||
message := "Test Message"
|
||||
err := context.Canceled
|
||||
|
||||
pipelineErr := PipelineError{
|
||||
Title: title,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
|
||||
if pipelineErr.Title != title {
|
||||
t.Errorf("title should be %q, got %q", title, pipelineErr.Title)
|
||||
}
|
||||
if pipelineErr.Message != message {
|
||||
t.Errorf("message should be %q, got %q", message, pipelineErr.Message)
|
||||
}
|
||||
if pipelineErr.Err != err {
|
||||
t.Errorf("err should be %v, got %v", err, pipelineErr.Err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pipeline error with nil error", func(t *testing.T) {
|
||||
pipelineErr := PipelineError{
|
||||
Title: "Test",
|
||||
Message: "Test message",
|
||||
Err: nil,
|
||||
}
|
||||
|
||||
if pipelineErr.Err != nil {
|
||||
t.Error("error should be nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPipelineConcurrency(t *testing.T) {
|
||||
t.Run("concurrent status reads", func(t *testing.T) {
|
||||
p := New()
|
||||
ctx := context.Background()
|
||||
|
||||
p.Run(ctx)
|
||||
defer p.Stop()
|
||||
|
||||
// Read status from multiple goroutines
|
||||
done := make(chan bool, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
for j := 0; j < 100; j++ {
|
||||
_ = p.Status()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines to finish
|
||||
for i := 0; i < 10; i++ {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timeout waiting for concurrent status reads")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concurrent action sends", func(t *testing.T) {
|
||||
p := New()
|
||||
ctx := context.Background()
|
||||
|
||||
p.Run(ctx)
|
||||
defer p.Stop()
|
||||
|
||||
actionCh := p.GetActionCh()
|
||||
|
||||
// Send actions from multiple goroutines
|
||||
done := make(chan bool, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
select {
|
||||
case actionCh <- Inject:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines to finish
|
||||
for i := 0; i < 5; i++ {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timeout waiting for concurrent action sends")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Mock implementations for testing without external dependencies
|
||||
type mockRecorder struct {
|
||||
started bool
|
||||
stopped bool
|
||||
frames chan mockFrame
|
||||
errors chan error
|
||||
}
|
||||
|
||||
type mockFrame struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (m *mockRecorder) Start(ctx context.Context) (<-chan mockFrame, <-chan error, error) {
|
||||
m.started = true
|
||||
m.frames = make(chan mockFrame, 10)
|
||||
m.errors = make(chan error, 1)
|
||||
|
||||
// Send some mock frames
|
||||
go func() {
|
||||
defer close(m.frames)
|
||||
defer close(m.errors)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case m.frames <- mockFrame{data: []byte("mock audio data")}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
return m.frames, m.errors, nil
|
||||
}
|
||||
|
||||
func (m *mockRecorder) Stop() error {
|
||||
m.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPipelineWithMocks(t *testing.T) {
|
||||
t.Run("mock recorder lifecycle", func(t *testing.T) {
|
||||
recorder := &mockRecorder{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
frames, errors, err := recorder.Start(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("mock recorder start failed: %v", err)
|
||||
}
|
||||
|
||||
if !recorder.started {
|
||||
t.Error("recorder should be marked as started")
|
||||
}
|
||||
|
||||
// Receive some frames
|
||||
frameCount := 0
|
||||
for frame := range frames {
|
||||
frameCount++
|
||||
if len(frame.data) == 0 {
|
||||
t.Error("frame should have data")
|
||||
}
|
||||
}
|
||||
|
||||
if frameCount == 0 {
|
||||
t.Error("should receive some frames")
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
select {
|
||||
case err := <-errors:
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
err = recorder.Stop()
|
||||
if err != nil {
|
||||
t.Errorf("mock recorder stop failed: %v", err)
|
||||
}
|
||||
|
||||
if !recorder.stopped {
|
||||
t.Error("recorder should be marked as stopped")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user