- Add logs and debug end-to-end tests - Add test helper utilities - Improve test fixtures and templates - Update API server and config lint commands - Add multi-user database initialization
345 lines
8.4 KiB
Go
345 lines
8.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|