package helpers import ( "testing" "github.com/jfraeys/fetch_ml/internal/api/helpers" "github.com/jfraeys/fetch_ml/internal/queue" ) func TestNewTaskErrorMapper(t *testing.T) { mapper := helpers.NewTaskErrorMapper() if mapper == nil { t.Error("NewTaskErrorMapper() returned nil") } } func TestTaskErrorMapper_MapJupyterError(t *testing.T) { mapper := helpers.NewTaskErrorMapper() tests := []struct { name string task *queue.Task want helpers.ErrorCode }{ { name: "nil task", task: nil, want: 0x00, // ErrorCodeUnknownError }, { name: "cancelled task", task: &queue.Task{Status: "cancelled"}, want: 0x24, // ErrorCodeJobCancelled }, { name: "oom error", task: &queue.Task{Status: "failed", Error: "out of memory"}, want: 0x30, // ErrorCodeOutOfMemory }, { name: "oom shorthand", task: &queue.Task{Status: "failed", Error: "OOM killed"}, want: 0x30, // ErrorCodeOutOfMemory }, { name: "disk full", task: &queue.Task{Status: "failed", Error: "no space left on device"}, want: 0x31, // ErrorCodeDiskFull }, { name: "disk full alt", task: &queue.Task{Status: "failed", Error: "disk full"}, want: 0x31, // ErrorCodeDiskFull }, { name: "rate limit", task: &queue.Task{Status: "failed", Error: "rate limit exceeded"}, want: 0x33, // ErrorCodeServiceUnavailable }, { name: "throttle", task: &queue.Task{Status: "failed", Error: "request throttled"}, want: 0x33, // ErrorCodeServiceUnavailable }, { name: "timeout", task: &queue.Task{Status: "failed", Error: "timed out waiting"}, want: 0x14, // ErrorCodeTimeout }, { name: "deadline", task: &queue.Task{Status: "failed", Error: "context deadline exceeded"}, want: 0x14, // ErrorCodeTimeout }, { name: "connection refused", task: &queue.Task{Status: "failed", Error: "connection refused"}, want: 0x12, // ErrorCodeNetworkError }, { name: "connection reset", task: &queue.Task{Status: "failed", Error: "connection reset by peer"}, want: 0x12, // ErrorCodeNetworkError }, { name: "network unreachable", task: &queue.Task{Status: "failed", Error: "network unreachable"}, want: 0x12, // ErrorCodeNetworkError }, { name: "queue not configured", task: &queue.Task{Status: "failed", Error: "queue not configured"}, want: 0x32, // ErrorCodeInvalidConfiguration }, { name: "generic failed", task: &queue.Task{Status: "failed", Error: "something went wrong"}, want: 0x23, // ErrorCodeJobExecutionFailed }, { name: "unknown status", task: &queue.Task{Status: "unknown", Error: "unknown error"}, want: 0x00, // ErrorCodeUnknownError }, { name: "case insensitive - cancelled", task: &queue.Task{Status: "CANCELLED"}, want: 0x24, // ErrorCodeJobCancelled }, { name: "case insensitive - oom", task: &queue.Task{Status: "FAILED", Error: "OUT OF MEMORY"}, want: 0x30, // ErrorCodeOutOfMemory }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := mapper.MapJupyterError(tt.task) if got != tt.want { t.Errorf("MapJupyterError() = 0x%02X, want 0x%02X", got, tt.want) } }) } } func TestTaskErrorMapper_MapError(t *testing.T) { mapper := helpers.NewTaskErrorMapper() tests := []struct { name string task *queue.Task defaultCode helpers.ErrorCode want helpers.ErrorCode }{ { name: "nil task returns default", task: nil, defaultCode: 0x11, // ErrorCodeDatabaseError want: 0x11, }, { name: "cancelled", task: &queue.Task{Status: "cancelled"}, defaultCode: 0x00, want: 0x24, // ErrorCodeJobCancelled }, { name: "oom", task: &queue.Task{Status: "failed", Error: "oom"}, defaultCode: 0x00, want: 0x30, // ErrorCodeOutOfMemory }, { name: "timeout", task: &queue.Task{Status: "failed", Error: "timeout"}, defaultCode: 0x00, want: 0x14, // ErrorCodeTimeout }, { name: "generic failed", task: &queue.Task{Status: "failed", Error: "generic error"}, defaultCode: 0x00, want: 0x23, // ErrorCodeJobExecutionFailed }, { name: "unknown status returns default", task: &queue.Task{Status: "weird", Error: "unknown"}, defaultCode: 0x01, want: 0x01, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := mapper.MapError(tt.task, tt.defaultCode) if got != tt.want { t.Errorf("MapError() = 0x%02X, want 0x%02X", got, tt.want) } }) } } func TestParseResourceRequest(t *testing.T) { tests := []struct { name string payload []byte want *helpers.ResourceRequest wantErr bool }{ { name: "empty payload", payload: []byte{}, want: nil, wantErr: false, }, { name: "nil payload", payload: nil, want: nil, wantErr: false, }, { name: "valid minimal", payload: []byte{4, 8, 1, 0}, want: &helpers.ResourceRequest{CPU: 4, MemoryGB: 8, GPU: 1, GPUMemory: ""}, wantErr: false, }, { name: "valid with gpu memory", payload: []byte{8, 16, 2, 4, '8', 'G', 'B', '!'}, want: &helpers.ResourceRequest{CPU: 8, MemoryGB: 16, GPU: 2, GPUMemory: "8GB!"}, wantErr: false, }, { name: "too short", payload: []byte{1, 2}, want: nil, wantErr: true, }, { name: "invalid gpu mem length", payload: []byte{1, 2, 1, 10, 'a'}, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := helpers.ParseResourceRequest(tt.payload) if (err != nil) != tt.wantErr { t.Errorf("ParseResourceRequest() error = %v, wantErr %v", err, tt.wantErr) return } if tt.want == nil { if got != nil { t.Errorf("ParseResourceRequest() = %v, want nil", got) } } else if got == nil { t.Errorf("ParseResourceRequest() = nil, want %v", tt.want) } else { if got.CPU != tt.want.CPU { t.Errorf("CPU = %d, want %d", got.CPU, tt.want.CPU) } if got.MemoryGB != tt.want.MemoryGB { t.Errorf("MemoryGB = %d, want %d", got.MemoryGB, tt.want.MemoryGB) } if got.GPU != tt.want.GPU { t.Errorf("GPU = %d, want %d", got.GPU, tt.want.GPU) } if got.GPUMemory != tt.want.GPUMemory { t.Errorf("GPUMemory = %q, want %q", got.GPUMemory, tt.want.GPUMemory) } } }) } } func TestMarshalJSONOrEmpty(t *testing.T) { tests := []struct { name string data interface{} want []byte }{ { name: "simple map", data: map[string]string{"key": "value"}, want: []byte(`{"key":"value"}`), }, { name: "string slice", data: []string{"a", "b", "c"}, want: []byte(`["a","b","c"]`), }, { name: "empty slice", data: []int{}, want: []byte(`[]`), }, { name: "nil", data: nil, want: []byte(`null`), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := helpers.MarshalJSONOrEmpty(tt.data) // For valid JSON, compare strings since JSON formatting might vary gotStr := string(got) wantStr := string(tt.want) if gotStr != wantStr { t.Errorf("MarshalJSONOrEmpty() = %q, want %q", gotStr, wantStr) } }) } } func TestMarshalJSONOrEmpty_ErrorCase(t *testing.T) { // Test with a value that can't be marshaled (function) got := helpers.MarshalJSONOrEmpty(func() {}) want := []byte("[]") if string(got) != string(want) { t.Errorf("MarshalJSONOrEmpty() with invalid data = %q, want %q", string(got), string(want)) } } func TestMarshalJSONBytes(t *testing.T) { data := map[string]int{"count": 42} got, err := helpers.MarshalJSONBytes(data) if err != nil { t.Errorf("MarshalJSONBytes() unexpected error: %v", err) } want := `{"count":42}` if string(got) != want { t.Errorf("MarshalJSONBytes() = %q, want %q", string(got), want) } } func TestIsEmptyJSON(t *testing.T) { tests := []struct { name string data []byte want bool }{ {"empty", []byte{}, true}, {"null", []byte("null"), true}, {"empty array", []byte("[]"), true}, {"empty object", []byte("{}"), true}, {"whitespace", []byte(" "), true}, {"data", []byte(`{"key":"value"}`), false}, {"non-empty array", []byte("[1,2,3]"), false}, {"null with spaces", []byte(" null "), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := helpers.IsEmptyJSON(tt.data); got != tt.want { t.Errorf("IsEmptyJSON() = %v, want %v", got, tt.want) } }) } }