calls_summarise_test.go
package calls
import (
"reflect"
"testing"
"skraak/datafile"
)
func TestFilterLabels(t *testing.T) {
a := &datafile.Label{Filter: "kiwi.txt", Species: "Kiwi"}
b := &datafile.Label{Filter: "tomtit.txt", Species: "Tomtit"}
c := &datafile.Label{Filter: "kiwi.txt", Species: "Kiwi2"}
labels := []*datafile.Label{a, b, c}
tests := []struct {
name string
filter string
want []*datafile.Label
}{
{"no filter returns all", "", labels},
{"matching filter returns subset", "kiwi.txt", []*datafile.Label{a, c}},
{"non-matching filter returns nil", "missing.txt", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := filterLabels(labels, tt.filter)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %v want %v", got, tt.want)
}
})
}
}
func TestBuildLabelSummaries_OmitsEmptyFields(t *testing.T) {
labels := []*datafile.Label{
{Filter: "f", Certainty: 80, Species: "Sp"},
{Filter: "f", Certainty: 100, Species: "Sp", CallType: "song", Comment: "hi", Bookmark: true},
}
got := buildLabelSummaries(labels)
if len(got) != 2 {
t.Fatalf("len=%d want 2", len(got))
}
if got[0].CallType != "" || got[0].Comment != "" || got[0].Bookmark {
t.Errorf("empty fields leaked: %+v", got[0])
}
if got[1].CallType != "song" || got[1].Comment != "hi" || !got[1].Bookmark {
t.Errorf("populated fields wrong: %+v", got[1])
}
}
func TestUpdateReviewStatus(t *testing.T) {
out := &CallsSummariseOutput{}
labels := []*datafile.Label{
{Certainty: 100},
{Certainty: 100, CallType: "song"},
{Certainty: 0},
{Certainty: 50, Comment: "hmm", Bookmark: true},
}
for _, l := range labels {
updateReviewStatus(l, out)
}
want := ReviewStatus{
Confirmed: 2,
DontKnow: 1,
Unreviewed: 1,
WithCallType: 1,
WithComments: 1,
Bookmarked: 1,
}
if out.ReviewStatus != want {
t.Errorf("got %+v want %+v", out.ReviewStatus, want)
}
}
func TestUpdateFilterStats_AccumulatesAcrossLabels(t *testing.T) {
out := &CallsSummariseOutput{Filters: map[string]FilterStats{}}
labels := []*datafile.Label{
{Filter: "kiwi.txt", Species: "Kiwi"},
{Filter: "kiwi.txt", Species: "Kiwi", CallType: "brrr"},
{Filter: "kiwi.txt", Species: "Kiwi", CallType: "brrr"},
{Filter: "kiwi.txt", Species: "Kiwi2"},
{Filter: "tomtit.txt", Species: "Tomtit", CallType: "song"},
}
for _, l := range labels {
updateFilterStats(l, out)
}
kiwi := out.Filters["kiwi.txt"]
if kiwi.Segments != 4 {
t.Errorf("kiwi.Segments=%d want 4", kiwi.Segments)
}
if kiwi.Species["Kiwi"] != 3 || kiwi.Species["Kiwi2"] != 1 {
t.Errorf("kiwi.Species=%v", kiwi.Species)
}
if kiwi.Calltypes["Kiwi"]["brrr"] != 2 {
t.Errorf("kiwi calltype brrr=%d want 2", kiwi.Calltypes["Kiwi"]["brrr"])
}
tomtit := out.Filters["tomtit.txt"]
if tomtit.Segments != 1 || tomtit.Calltypes["Tomtit"]["song"] != 1 {
t.Errorf("tomtit=%+v", tomtit)
}
}
func TestTrackMeta(t *testing.T) {
ops, revs := map[string]bool{}, map[string]bool{}
trackMeta(nil, ops, revs)
if len(ops) != 0 || len(revs) != 0 {
t.Errorf("nil meta should be no-op")
}
trackMeta(&datafile.DataMeta{Operator: "alice", Reviewer: ""}, ops, revs)
trackMeta(&datafile.DataMeta{Operator: "", Reviewer: "bob"}, ops, revs)
trackMeta(&datafile.DataMeta{Operator: "alice", Reviewer: "bob"}, ops, revs)
if !ops["alice"] || len(ops) != 1 {
t.Errorf("operators=%v want only alice", ops)
}
if !revs["bob"] || len(revs) != 1 {
t.Errorf("reviewers=%v want only bob", revs)
}
}
func TestExtractRelativePath(t *testing.T) {
tests := []struct {
dataPath string
want string
}{
{"/folder/tx51_LISTENING_20260221_203004.WAV.data", "tx51_LISTENING_20260221_203004.WAV"},
{"foo.wav.data", "foo.wav"},
{"/a/b/c/file.WAV.data", "file.WAV"},
{"noslash.data", "noslash"},
}
for _, tt := range tests {
t.Run(tt.dataPath, func(t *testing.T) {
got := extractRelativePath("", tt.dataPath)
if got != tt.want {
t.Errorf("got %q want %q", got, tt.want)
}
})
}
}
func TestUpdateStatsFromLabels_DelegatesCorrectly(t *testing.T) {
out := &CallsSummariseOutput{Filters: map[string]FilterStats{}}
labels := []*datafile.Label{
{Filter: "f1", Species: "Kiwi", Certainty: 100, CallType: "song"},
{Filter: "f1", Species: "Kiwi", Certainty: 0},
}
updateStatsFromLabels(labels, out)
// Should have delegated to both updateFilterStats and updateReviewStatus
if out.Filters["f1"].Segments != 2 {
t.Errorf("Segments=%d want 2", out.Filters["f1"].Segments)
}
if out.ReviewStatus.Confirmed != 1 {
t.Errorf("Confirmed=%d want 1", out.ReviewStatus.Confirmed)
}
if out.ReviewStatus.DontKnow != 1 {
t.Errorf("DontKnow=%d want 1", out.ReviewStatus.DontKnow)
}
if out.ReviewStatus.WithCallType != 1 {
t.Errorf("WithCallType=%d want 1", out.ReviewStatus.WithCallType)
}
}
func TestFinaliseSummary_SortsAndClearsEmptyCalltypes(t *testing.T) {
out := &CallsSummariseOutput{
Filters: map[string]FilterStats{
"f1": {Segments: 1, Species: map[string]int{"S": 1}, Calltypes: map[string]map[string]int{}},
"f2": {Segments: 1, Species: map[string]int{"S": 1}, Calltypes: map[string]map[string]int{"S": {"c": 1}}},
},
Segments: []SegmentSummary{
{File: "b.wav", StartTime: 1},
{File: "a.wav", StartTime: 5},
{File: "a.wav", StartTime: 2},
},
}
finaliseSummary(out, map[string]bool{"zoe": true, "alice": true}, map[string]bool{"bob": true}, false)
if out.Filters["f1"].Calltypes != nil {
t.Errorf("empty calltypes not cleared: %v", out.Filters["f1"].Calltypes)
}
if out.Filters["f2"].Calltypes == nil {
t.Errorf("populated calltypes cleared in error")
}
wantOps := []string{"alice", "zoe"}
if !reflect.DeepEqual(out.Operators, wantOps) {
t.Errorf("operators=%v want %v", out.Operators, wantOps)
}
wantOrder := []struct {
file string
start float64
}{{"a.wav", 2}, {"a.wav", 5}, {"b.wav", 1}}
for i, w := range wantOrder {
if out.Segments[i].File != w.file || out.Segments[i].StartTime != w.start {
t.Errorf("segment[%d]=%+v want file=%s start=%v", i, out.Segments[i], w.file, w.start)
}
}
}