calls_add_test.go
package calls
import (
"os"
"path/filepath"
"testing"
"skraak/datafile"
)
func TestCallsAddNewFile(t *testing.T) {
tmpDir := t.TempDir()
wavPath := filepath.Join(tmpDir, "test.wav")
dataPath := wavPath + ".data"
createTestWAV(t, wavPath, 16000, 10)
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "2-5",
Species: "Kiwi",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Created {
t.Errorf("expected Created=true, got false")
}
if result.SegmentStart != 2 {
t.Errorf("expected segment_start=2, got %f", result.SegmentStart)
}
if result.SegmentEnd != 5 {
t.Errorf("expected segment_end=5, got %f", result.SegmentEnd)
}
if result.LowFreq != 0 {
t.Errorf("expected low_freq=0, got %f", result.LowFreq)
}
if result.HighFreq != 16000 {
t.Errorf("expected high_freq=16000, got %f", result.HighFreq)
}
if result.Species != "Kiwi" {
t.Errorf("expected species=Kiwi, got %s", result.Species)
}
if result.Filter != "Manual" {
t.Errorf("expected filter=Manual, got %s", result.Filter)
}
}
func TestCallsAddNewFileMetadata(t *testing.T) {
tmpDir := t.TempDir()
wavPath := filepath.Join(tmpDir, "test.wav")
dataPath := wavPath + ".data"
createTestWAV(t, wavPath, 16000, 10)
_, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "2-5",
Species: "Kiwi",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, err := os.Stat(dataPath); os.IsNotExist(err) {
t.Fatal("expected .data file to be created")
}
df, err := datafile.ParseDataFile(dataPath)
if err != nil {
t.Fatalf("failed to parse created file: %v", err)
}
if df.Meta.Operator != "Manual" {
t.Errorf("expected Operator=Manual, got %s", df.Meta.Operator)
}
if df.Meta.Reviewer != "David" {
t.Errorf("expected Reviewer=David, got %s", df.Meta.Reviewer)
}
if len(df.Segments) != 1 {
t.Fatalf("expected 1 segment, got %d", len(df.Segments))
}
seg := df.Segments[0]
if seg.StartTime != 2 || seg.EndTime != 5 {
t.Errorf("expected segment 2-5, got %.1f-%.1f", seg.StartTime, seg.EndTime)
}
if len(seg.Labels) != 1 {
t.Fatalf("expected 1 label, got %d", len(seg.Labels))
}
if seg.Labels[0].Species != "Kiwi" {
t.Errorf("expected species=Kiwi, got %s", seg.Labels[0].Species)
}
}
func TestCallsAddToExistingFile(t *testing.T) {
tmpDir := t.TempDir()
wavPath := filepath.Join(tmpDir, "test.wav")
dataPath := wavPath + ".data"
createTestWAV(t, wavPath, 16000, 10)
df := &datafile.DataFile{
Meta: &datafile.DataMeta{Operator: "BirdNET", Duration: 10, Reviewer: "AI"},
Segments: []*datafile.Segment{
{
StartTime: 2,
EndTime: 5,
FreqLow: 0,
FreqHigh: 16000,
Labels: []*datafile.Label{
{Species: "Tui", Certainty: 80, Filter: "BirdNET"},
},
},
},
}
if err := df.Write(dataPath); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "2-5",
Species: "Kiwi",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Created {
t.Errorf("expected Created=false (label added to existing segment), got true")
}
df2, err := datafile.ParseDataFile(dataPath)
if err != nil {
t.Fatalf("failed to parse file: %v", err)
}
if len(df2.Segments) != 1 {
t.Fatalf("expected 1 segment, got %d", len(df2.Segments))
}
if len(df2.Segments[0].Labels) != 2 {
t.Errorf("expected 2 labels, got %d", len(df2.Segments[0].Labels))
}
if df2.Meta.Reviewer != "David" {
t.Errorf("expected Reviewer=David, got %s", df2.Meta.Reviewer)
}
if df2.Meta.Operator != "BirdNET" {
t.Errorf("expected Operator=BirdNET (unchanged), got %s", df2.Meta.Operator)
}
}
func TestCallsAddDuplicateFilter(t *testing.T) {
tmpDir := t.TempDir()
dataPath := filepath.Join(tmpDir, "test.data")
df := &datafile.DataFile{
Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
Segments: []*datafile.Segment{
{
StartTime: 2,
EndTime: 5,
FreqLow: 0,
FreqHigh: 16000,
Labels: []*datafile.Label{
{Species: "Kiwi", Certainty: 80, Filter: "Manual"},
},
},
},
}
if err := df.Write(dataPath); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "2-5",
Species: "Tui",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err == nil {
t.Error("expected error for duplicate filter, got nil")
}
if result.Error == "" {
t.Error("expected error message in output")
}
}
func TestCallsAddNewSegment(t *testing.T) {
tmpDir := t.TempDir()
dataPath := filepath.Join(tmpDir, "test.data")
df := &datafile.DataFile{
Meta: &datafile.DataMeta{Operator: "Manual", Duration: 60},
Segments: []*datafile.Segment{
{
StartTime: 2,
EndTime: 5,
FreqLow: 0,
FreqHigh: 16000,
Labels: []*datafile.Label{
{Species: "Kiwi", Certainty: 80, Filter: "Manual"},
},
},
},
}
if err := df.Write(dataPath); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "10-15",
Frequency: "200-4500",
Species: "Tui",
Certainty: 90,
Filter: "Manual",
Reviewer: "David",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Created {
t.Errorf("expected Created=true, got false")
}
if result.LowFreq != 200 {
t.Errorf("expected low_freq=200, got %f", result.LowFreq)
}
if result.HighFreq != 4500 {
t.Errorf("expected high_freq=4500, got %f", result.HighFreq)
}
df2, err := datafile.ParseDataFile(dataPath)
if err != nil {
t.Fatalf("failed to parse file: %v", err)
}
if len(df2.Segments) != 2 {
t.Errorf("expected 2 segments, got %d", len(df2.Segments))
}
}
func TestCallsAddWithCallType(t *testing.T) {
tmpDir := t.TempDir()
wavPath := filepath.Join(tmpDir, "test.wav")
dataPath := wavPath + ".data"
createTestWAV(t, wavPath, 16000, 10)
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "2-5",
Species: "Kiwi+Duet",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Species != "Kiwi" {
t.Errorf("expected species=Kiwi, got %s", result.Species)
}
if result.CallType != "Duet" {
t.Errorf("expected calltype=Duet, got %s", result.CallType)
}
}
func TestCallsAddMissingWAV(t *testing.T) {
tmpDir := t.TempDir()
dataPath := filepath.Join(tmpDir, "nonexistent.wav.data")
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "2-5",
Species: "Kiwi",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err == nil {
t.Error("expected error when WAV file missing, got nil")
}
if result.Error == "" {
t.Error("expected error message in output")
}
}
func TestCallsAddMissingSpecies(t *testing.T) {
result, err := CallsAdd(CallsAddInput{
File: "test.data",
Segment: "2-5",
Reviewer: "David",
})
if err == nil {
t.Error("expected error for missing species, got nil")
}
if result.Error == "" {
t.Error("expected error message in output")
}
}
func TestCallsAddClampsToDuration(t *testing.T) {
tmpDir := t.TempDir()
wavPath := filepath.Join(tmpDir, "test.wav")
dataPath := wavPath + ".data"
createTestWAV(t, wavPath, 16000, 10)
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "8-15",
Species: "sample",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Clamped {
t.Error("expected Clamped=true when end exceeds duration")
}
if result.SegmentEnd != 10.0 {
t.Errorf("expected segment_end clamped to 10.0, got %f", result.SegmentEnd)
}
if result.SegmentStart != 8.0 {
t.Errorf("expected segment_start=8.0, got %f", result.SegmentStart)
}
df, err := datafile.ParseDataFile(dataPath)
if err != nil {
t.Fatalf("failed to parse file: %v", err)
}
if len(df.Segments) != 1 {
t.Fatalf("expected 1 segment, got %d", len(df.Segments))
}
if df.Segments[0].EndTime != 10.0 {
t.Errorf("expected persisted EndTime=10.0, got %f", df.Segments[0].EndTime)
}
}
func TestCallsAddRejectsStartAtOrPastDuration(t *testing.T) {
tmpDir := t.TempDir()
wavPath := filepath.Join(tmpDir, "test.wav")
dataPath := wavPath + ".data"
createTestWAV(t, wavPath, 16000, 10)
cases := []struct {
name string
segment string
}{
{"start equals duration", "10-15"},
{"start past duration", "12-15"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: tc.segment,
Species: "sample",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err == nil {
t.Errorf("expected error for segment %s, got nil", tc.segment)
}
if result.Error == "" {
t.Error("expected error message in output")
}
if _, statErr := os.Stat(dataPath); !os.IsNotExist(statErr) {
t.Error(".data file should not be created for impossible segment")
}
})
}
}
func TestCallsAddExactDurationBoundary(t *testing.T) {
tmpDir := t.TempDir()
wavPath := filepath.Join(tmpDir, "test.wav")
dataPath := wavPath + ".data"
createTestWAV(t, wavPath, 16000, 10)
result, err := CallsAdd(CallsAddInput{
File: dataPath,
Segment: "0-10",
Species: "sample",
Certainty: 100,
Filter: "Manual",
Reviewer: "David",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Clamped {
t.Error("expected Clamped=false when end == duration exactly")
}
if result.SegmentEnd != 10.0 {
t.Errorf("expected segment_end=10.0, got %f", result.SegmentEnd)
}
}
// createTestWAV creates a minimal valid WAV file for testing.
func createTestWAV(t *testing.T, path string, sampleRate int, durationSec int) {
t.Helper()
numSamples := sampleRate * durationSec
dataSize := numSamples * 2 // 16-bit = 2 bytes per sample
fileSize := 36 + dataSize
f, err := os.Create(path)
if err != nil {
t.Fatalf("failed to create WAV file: %v", err)
}
defer f.Close()
f.Write([]byte("RIFF"))
writeUint32(f, uint32(fileSize))
f.Write([]byte("WAVE"))
f.Write([]byte("fmt "))
writeUint32(f, 16)
writeUint16(f, 1)
writeUint16(f, 1)
writeUint32(f, uint32(sampleRate))
writeUint32(f, uint32(sampleRate*2))
writeUint16(f, 2)
writeUint16(f, 16)
f.Write([]byte("data"))
writeUint32(f, uint32(dataSize))
silence := make([]byte, dataSize)
f.Write(silence)
}
func writeUint32(f *os.File, v uint32) {
buf := make([]byte, 4)
buf[0] = byte(v)
buf[1] = byte(v >> 8)
buf[2] = byte(v >> 16)
buf[3] = byte(v >> 24)
f.Write(buf)
}
func writeUint16(f *os.File, v uint16) {
buf := make([]byte, 2)
buf[0] = byte(v)
buf[1] = byte(v >> 8)
f.Write(buf)
}