G7A4W2SHFK3NOI446VXTZWXPWGCM3QV6LOOF53WNRDE27KJ25LJQC package utilsimport ("os""path/filepath""testing")func TestLoadMappingFile(t *testing.T) {t.Run("valid mapping", func(t *testing.T) {content := `{"GSK": {"species": "Roroa", "calltypes": {"Male": "Male - Solo"}},"Don't Know": {"species": "Don't Know"}}`path := createTempFile(t, content)defer os.Remove(path)mapping, err := LoadMappingFile(path)if err != nil {t.Fatalf("expected no error, got: %v", err)}if len(mapping) != 2 {t.Errorf("expected 2 entries, got %d", len(mapping))}if mapping["GSK"].Species != "Roroa" {t.Errorf("expected GSK -> Roroa, got %s", mapping["GSK"].Species)}if mapping["GSK"].Calltypes["Male"] != "Male - Solo" {t.Errorf("expected GSK Male -> Male - Solo, got %s", mapping["GSK"].Calltypes["Male"])}})t.Run("invalid JSON", func(t *testing.T) {content := `{invalid json}`path := createTempFile(t, content)defer os.Remove(path)_, err := LoadMappingFile(path)if err == nil {t.Fatal("expected error for invalid JSON")}})t.Run("empty file", func(t *testing.T) {content := `{}`path := createTempFile(t, content)defer os.Remove(path)_, err := LoadMappingFile(path)if err == nil {t.Fatal("expected error for empty mapping")}})t.Run("missing species field", func(t *testing.T) {content := `{"GSK": {"calltypes": {"Male": "Male - Solo"}}}`path := createTempFile(t, content)defer os.Remove(path)_, err := LoadMappingFile(path)if err == nil {t.Fatal("expected error for missing species field")}})t.Run("empty species field", func(t *testing.T) {content := `{"GSK": {"species": ""}}`path := createTempFile(t, content)defer os.Remove(path)_, err := LoadMappingFile(path)if err == nil {t.Fatal("expected error for empty species field")}})t.Run("nonexistent file", func(t *testing.T) {_, err := LoadMappingFile("/nonexistent/path/mapping.json")if err == nil {t.Fatal("expected error for nonexistent file")}})}func TestGetDBSpecies(t *testing.T) {mapping := MappingFile{"GSK": {Species: "Roroa"},"K-M": {Species: "Kiwi"},}t.Run("found", func(t *testing.T) {species, ok := mapping.GetDBSpecies("GSK")if !ok {t.Fatal("expected to find GSK")}if species != "Roroa" {t.Errorf("expected Roroa, got %s", species)}})t.Run("not found", func(t *testing.T) {_, ok := mapping.GetDBSpecies("UNKNOWN")if ok {t.Fatal("expected not to find UNKNOWN")}})}func TestGetDBCalltype(t *testing.T) {mapping := MappingFile{"GSK": {Species: "Roroa",Calltypes: map[string]string{"Male": "Male - Solo","Female": "Female - Solo",},},"K-M": {Species: "Kiwi"}, // no calltype mapping}t.Run("with mapping", func(t *testing.T) {ct := mapping.GetDBCalltype("GSK", "Male")if ct != "Male - Solo" {t.Errorf("expected 'Male - Solo', got %s", ct)}})t.Run("without mapping - passthrough", func(t *testing.T) {ct := mapping.GetDBCalltype("GSK", "Unknown")if ct != "Unknown" {t.Errorf("expected passthrough 'Unknown', got %s", ct)}})t.Run("species not in mapping - passthrough", func(t *testing.T) {ct := mapping.GetDBCalltype("UNKNOWN", "Male")if ct != "Male" {t.Errorf("expected passthrough 'Male', got %s", ct)}})t.Run("species without calltypes - passthrough", func(t *testing.T) {ct := mapping.GetDBCalltype("K-M", "Male")if ct != "Male" {t.Errorf("expected passthrough 'Male', got %s", ct)}})}func TestMappingValidationResult(t *testing.T) {t.Run("HasErrors - no errors", func(t *testing.T) {r := MappingValidationResult{}if r.HasErrors() {t.Error("expected no errors")}})t.Run("HasErrors - missing species", func(t *testing.T) {r := MappingValidationResult{MissingSpecies: []string{"UNKNOWN"}}if !r.HasErrors() {t.Error("expected errors")}})t.Run("HasErrors - missing DB species", func(t *testing.T) {r := MappingValidationResult{MissingDBSpecies: []string{"Phantom"}}if !r.HasErrors() {t.Error("expected errors")}})t.Run("HasErrors - missing calltypes", func(t *testing.T) {r := MappingValidationResult{MissingCalltypes: map[string]string{"GSK/Male": "Roroa/Male - Solo"}}if !r.HasErrors() {t.Error("expected errors")}})t.Run("Error - all error types", func(t *testing.T) {r := MappingValidationResult{MissingSpecies: []string{"UNKNOWN"},MissingDBSpecies: []string{"Phantom"},MissingCalltypes: map[string]string{"GSK/Male": "Roroa/Male - Solo"},}errStr := r.Error()if errStr == "" {t.Error("expected non-empty error string")}// Check all parts are presentif !containsSubstring(errStr, "UNKNOWN") {t.Error("error string should contain MISSING species")}if !containsSubstring(errStr, "Phantom") {t.Error("error string should contain missing DB species")}if !containsSubstring(errStr, "GSK/Male") {t.Error("error string should contain missing calltype")}})}// Helper functionsfunc createTempFile(t *testing.T, content string) string {t.Helper()tmpDir := t.TempDir()path := filepath.Join(tmpDir, "mapping.json")if err := os.WriteFile(path, []byte(content), 0644); err != nil {t.Fatalf("failed to create temp file: %v", err)}return path}func containsSubstring(s, substr string) bool {return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstringHelper(s, substr))}func containsSubstringHelper(s, substr string) bool {for i := 0; i <= len(s)-len(substr); i++ {if s[i:i+len(substr)] == substr {return true}}return false}
}}func TestSkraakHashRoundTrip(t *testing.T) {// Test that skraak_hash in metadata is preserved through parse/write cycledf := &DataFile{Meta: &DataMeta{Operator: "Test",Duration: 60.0,Extra: map[string]any{"skraak_hash": "abc123def456",},},Segments: []*Segment{{StartTime: 10.0,EndTime: 20.0,Labels: []*Label{{Species: "Kiwi", Certainty: 100, Filter: "M"},},},},}tmpfile, err := os.CreateTemp("", "test*.data")if err != nil {t.Fatal(err)}tmpfile.Close()defer os.Remove(tmpfile.Name())// Writeif err := df.Write(tmpfile.Name()); err != nil {t.Fatal(err)}// Re-parsedf2, err := ParseDataFile(tmpfile.Name())if err != nil {t.Fatal(err)}// Verify skraak_hash preservedif df2.Meta.Extra == nil {t.Fatal("expected Extra to be non-nil")}hash, ok := df2.Meta.Extra["skraak_hash"].(string)if !ok {t.Fatal("expected skraak_hash to be string")}if hash != "abc123def456" {t.Errorf("expected skraak_hash=abc123def456, got %s", hash)}}func TestSkraakLabelIDRoundTrip(t *testing.T) {// Test that skraak_label_id in labels is preserved through parse/write cycledf := &DataFile{Meta: &DataMeta{Operator: "Test",Duration: 60.0,},Segments: []*Segment{{StartTime: 10.0,EndTime: 20.0,Labels: []*Label{{Species: "Kiwi",Certainty: 100,Filter: "M",Extra: map[string]any{"skraak_label_id": "label_abc123",},},},},},}tmpfile, err := os.CreateTemp("", "test*.data")if err != nil {t.Fatal(err)}tmpfile.Close()defer os.Remove(tmpfile.Name())// Writeif err := df.Write(tmpfile.Name()); err != nil {t.Fatal(err)}// Re-parsedf2, err := ParseDataFile(tmpfile.Name())if err != nil {t.Fatal(err)}// Verify skraak_label_id preservedif len(df2.Segments) != 1 {t.Fatalf("expected 1 segment, got %d", len(df2.Segments))}if len(df2.Segments[0].Labels) != 1 {t.Fatalf("expected 1 label, got %d", len(df2.Segments[0].Labels))}label := df2.Segments[0].Labels[0]if label.Extra == nil {t.Fatal("expected label Extra to be non-nil")}labelID, ok := label.Extra["skraak_label_id"].(string)if !ok {t.Fatal("expected skraak_label_id to be string")}if labelID != "label_abc123" {t.Errorf("expected skraak_label_id=label_abc123, got %s", labelID)}}func TestSkraakFieldsBothPresent(t *testing.T) {// Test both skraak_hash and skraak_label_id togetherdf := &DataFile{Meta: &DataMeta{Operator: "Test",Duration: 60.0,Extra: map[string]any{"skraak_hash": "file_hash_xyz",},},Segments: []*Segment{{StartTime: 10.0,EndTime: 20.0,Labels: []*Label{{Species: "Kiwi",Certainty: 100,Filter: "M",Extra: map[string]any{"skraak_label_id": "label_id_1",},},{Species: "Roroa",Certainty: 90,Filter: "M",Extra: map[string]any{"skraak_label_id": "label_id_2",},},},},},}tmpfile, err := os.CreateTemp("", "test*.data")if err != nil {t.Fatal(err)}tmpfile.Close()defer os.Remove(tmpfile.Name())// Writeif err := df.Write(tmpfile.Name()); err != nil {t.Fatal(err)
// Re-parsedf2, err := ParseDataFile(tmpfile.Name())if err != nil {t.Fatal(err)}// Verify skraak_hashif df2.Meta.Extra["skraak_hash"] != "file_hash_xyz" {t.Errorf("expected skraak_hash=file_hash_xyz, got %v", df2.Meta.Extra["skraak_hash"])}// Verify both label IDsif len(df2.Segments[0].Labels) != 2 {t.Fatalf("expected 2 labels, got %d", len(df2.Segments[0].Labels))}labelIDs := []string{"label_id_1", "label_id_2"}for i, label := range df2.Segments[0].Labels {if label.Extra["skraak_label_id"] != labelIDs[i] {t.Errorf("label %d: expected skraak_label_id=%s, got %v", i, labelIDs[i], label.Extra["skraak_label_id"])}}
package toolsimport ("testing""skraak/utils")func TestValidateSegmentImportInput(t *testing.T) {t.Run("invalid dataset ID - too short", func(t *testing.T) {input := ImportSegmentsInput{DatasetID: "abc",}err := validateSegmentImportInput(input)if err == nil {t.Fatal("expected error for short dataset ID")}})t.Run("invalid dataset ID - too long", func(t *testing.T) {input := ImportSegmentsInput{DatasetID: "abc123def456ghi789",}err := validateSegmentImportInput(input)if err == nil {t.Fatal("expected error for long dataset ID")}})t.Run("invalid dataset ID - invalid characters", func(t *testing.T) {input := ImportSegmentsInput{DatasetID: "abc123!!!456",}err := validateSegmentImportInput(input)if err == nil {t.Fatal("expected error for invalid characters in dataset ID")}})t.Run("invalid location ID", func(t *testing.T) {input := ImportSegmentsInput{DatasetID: "abc123def456",LocationID: "invalid",}err := validateSegmentImportInput(input)if err == nil {t.Fatal("expected error for invalid location ID")}})t.Run("invalid cluster ID", func(t *testing.T) {input := ImportSegmentsInput{DatasetID: "abc123def456",LocationID: "xyz789uvw012",ClusterID: "invalid",}err := validateSegmentImportInput(input)if err == nil {t.Fatal("expected error for invalid cluster ID")}})}func TestCountTotalSegments(t *testing.T) {t.Run("empty", func(t *testing.T) {count := countTotalSegments(map[string]scannedDataFile{})if count != 0 {t.Errorf("expected 0, got %d", count)}})t.Run("single file - no segments", func(t *testing.T) {files := map[string]scannedDataFile{"file1": {Segments: []*utils.Segment{}},}count := countTotalSegments(files)if count != 0 {t.Errorf("expected 0, got %d", count)}})t.Run("single file - multiple segments", func(t *testing.T) {files := map[string]scannedDataFile{"file1": {Segments: []*utils.Segment{{}, {}, {}}},}count := countTotalSegments(files)if count != 3 {t.Errorf("expected 3, got %d", count)}})t.Run("multiple files", func(t *testing.T) {files := map[string]scannedDataFile{"file1": {Segments: []*utils.Segment{{}, {}}},"file2": {Segments: []*utils.Segment{{}}},"file3": {Segments: []*utils.Segment{{}, {}, {}, {}}},}count := countTotalSegments(files)if count != 7 {t.Errorf("expected 7, got %d", count)}})}
- `utils/mapping_test.go` — Unit tests for mapping file loading and validation- `tools/import_segments_test.go` — Unit tests for input validation and segment counting- `utils/data_file_test.go` — Added tests for skraak_hash and skraak_label_id round-trip